From 3cc9af06b4ab0bc9cdf151190e9a42860c394b8c Mon Sep 17 00:00:00 2001 From: mikereiche Date: Thu, 7 Oct 2021 19:00:38 -0700 Subject: [PATCH 1/9] Add support for Couchbase Transactions. The fluent operations are common for options that are common to operations with and without transactions. Once there is a transaction(ctx), or an option specific to without-transactions (such as scanConsistency), the interfaces are bifurcated, so that an non-transaction option cannot be applied to a transaction operation, and a transaction(ctx) cannot be applied where a non-transaction option has already been applied. Closes #1145. --- pom.xml | 9 + .../SimpleCouchbaseClientFactory.java | 4 +- .../AbstractCouchbaseConfiguration.java | 24 +- .../data/couchbase/config/BeanNames.java | 5 + .../core/AbstractTemplateSupport.java | 186 +++++++++++ .../core/CouchbaseTemplateSupport.java | 164 +--------- .../ExecutableFindByAnalyticsOperation.java | 101 +++--- ...utableFindByAnalyticsOperationSupport.java | 6 +- .../core/ExecutableFindByIdOperation.java | 73 +++-- .../ExecutableFindByIdOperationSupport.java | 29 +- .../core/ExecutableFindByQueryOperation.java | 142 +++++---- ...ExecutableFindByQueryOperationSupport.java | 43 ++- .../core/ExecutableInsertByIdOperation.java | 50 +-- .../ExecutableInsertByIdOperationSupport.java | 31 +- .../core/ExecutableRemoveByIdOperation.java | 46 +-- .../ExecutableRemoveByIdOperationSupport.java | 30 +- .../ExecutableRemoveByQueryOperation.java | 78 +++-- ...ecutableRemoveByQueryOperationSupport.java | 32 +- .../core/ExecutableReplaceByIdOperation.java | 42 ++- ...ExecutableReplaceByIdOperationSupport.java | 31 +- .../core/ExecutableUpsertByIdOperation.java | 33 +- .../ExecutableUpsertByIdOperationSupport.java | 2 +- .../core/NonReactiveSupportWrapper.java | 21 +- .../ReactiveCouchbaseTemplateSupport.java | 150 +-------- .../ReactiveExistsByIdOperationSupport.java | 4 +- .../ReactiveFindByAnalyticsOperation.java | 98 +++--- ...activeFindByAnalyticsOperationSupport.java | 8 +- .../core/ReactiveFindByIdOperation.java | 73 +++-- .../ReactiveFindByIdOperationSupport.java | 90 ++++-- .../core/ReactiveFindByQueryOperation.java | 131 ++++---- .../ReactiveFindByQueryOperationSupport.java | 97 ++++-- ...eFindFromReplicasByIdOperationSupport.java | 6 +- .../core/ReactiveInsertByIdOperation.java | 50 +-- .../ReactiveInsertByIdOperationSupport.java | 76 +++-- .../core/ReactiveRemoveByIdOperation.java | 44 +-- .../ReactiveRemoveByIdOperationSupport.java | 68 ++-- .../core/ReactiveRemoveByQueryOperation.java | 77 +++-- ...ReactiveRemoveByQueryOperationSupport.java | 59 +++- .../core/ReactiveReplaceByIdOperation.java | 41 ++- .../ReactiveReplaceByIdOperationSupport.java | 95 ++++-- .../core/ReactiveTemplateSupport.java | 10 +- .../core/ReactiveUpsertByIdOperation.java | 33 +- .../ReactiveUpsertByIdOperationSupport.java | 7 +- .../data/couchbase/core/TemplateSupport.java | 11 +- .../convert/MappingCouchbaseConverter.java | 8 + .../BasicCouchbasePersistentEntity.java | 10 + .../mapping/CouchbasePersistentEntity.java | 7 + .../couchbase/core/query/OptionsBuilder.java | 40 +++ .../couchbase/core/support/PseudoArgs.java | 25 +- .../data/couchbase/core/support/WithCas.java | 33 ++ .../core/support/WithTransaction.java | 33 ++ .../repository/DynamicProxyable.java | 17 +- .../repository/TransactionResult.java | 39 +++ .../support/DynamicInvocationHandler.java | 21 +- .../support/SimpleCouchbaseRepository.java | 12 +- .../SimpleReactiveCouchbaseRepository.java | 12 +- .../support/TransactionResultHolder.java | 51 +++ ...mplateQueryCollectionIntegrationTests.java | 158 +++++----- ...ouchbaseTemplateQueryIntegrationTests.java | 30 +- ...mplateQueryCollectionIntegrationTests.java | 122 ++++---- .../data/couchbase/domain/Airport.java | 1 + .../couchbase/domain/AirportRepository.java | 7 + .../data/couchbase/domain/Config.java | 2 +- .../data/couchbase/domain/Person.java | 47 ++- .../couchbase/domain/PersonRepository.java | 3 +- .../domain/ReactivePersonRepository.java | 27 ++ ...chbaseRepositoryQueryIntegrationTests.java | 58 ++-- ...chbaseRepositoryQueryIntegrationTests.java | 4 +- ...sitoryQueryCollectionIntegrationTests.java | 20 +- .../query/StringN1qlQueryCreatorTests.java | 7 +- .../CouchbaseTransactionNativeTests.java | 290 ++++++++++++++++++ .../util/ClusterAwareIntegrationTests.java | 7 +- .../couchbase/util/JavaIntegrationTests.java | 9 +- .../data/couchbase/util/TestCluster.java | 2 +- src/test/resources/logback.xml | 2 + 75 files changed, 2263 insertions(+), 1251 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithCas.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java diff --git a/pom.xml b/pom.xml index 6aac4a641..cfc94d39f 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ 3.2.5 2.7.0-SNAPSHOT spring.data.couchbase + 1.2.2 @@ -37,6 +38,14 @@ + + + com.couchbase.client + couchbase-transactions + ${couchbase-transactions} + + + org.springframework spring-context-support diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index b97b57f95..e19243718 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -97,9 +97,9 @@ public Scope getScope() { @Override public Collection getCollection(final String collectionName) { final Scope scope = getScope(); - if (collectionName == null) { + if (collectionName == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionName)) { if (!scope.name().equals(CollectionIdentifier.DEFAULT_SCOPE)) { - throw new IllegalStateException("A collectionName must be provided if a non-default scope is used!"); + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used"); } return getBucket().defaultCollection(); } diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 6982d64f4..84146860d 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -17,11 +17,15 @@ package org.springframework.data.couchbase.config; import static com.couchbase.client.java.ClusterOptions.clusterOptions; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_MAPPING_CONTEXT; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TRANSACTIONS; +import java.time.Duration; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -46,6 +50,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import com.couchbase.client.core.cnc.Event; import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature; import com.couchbase.client.core.encryption.CryptoManager; import com.couchbase.client.core.env.Authenticator; @@ -57,7 +62,10 @@ import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.json.JacksonTransformers; import com.couchbase.client.java.json.JsonValueModule; -import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -123,7 +131,7 @@ protected Authenticator authenticator() { * @param couchbaseCluster the cluster reference from the SDK. * @return the initialized factory. */ - @Bean + @Bean(name = BeanNames.COUCHBASE_CLIENT_FACTORY) public CouchbaseClientFactory couchbaseClientFactory(final Cluster couchbaseCluster) { return new SimpleCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); } @@ -282,7 +290,7 @@ public TranslationService couchbaseTranslationService() { * Creates a {@link CouchbaseMappingContext} equipped with entity classes scanned from the mapping base package. * */ - @Bean + @Bean(COUCHBASE_MAPPING_CONTEXT) public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customConversions) throws Exception { CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -310,6 +318,16 @@ public ObjectMapper couchbaseObjectMapper() { return mapper; } + @Bean(COUCHBASE_TRANSACTIONS) + public Transactions getTransactions(Cluster cluster) { + return Transactions.create(cluster, getTransactionConfig()); + } + + TransactionConfig getTransactionConfig() { + return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.NONE).build(); + } + /** * Configure whether to automatically create indices for domain types by deriving the from the entity or not. */ diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index cb9bf63ea..30040cb20 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -34,6 +34,8 @@ public class BeanNames { public static final String COUCHBASE_CUSTOM_CONVERSIONS = "couchbaseCustomConversions"; + public static final String COUCHBASE_TRANSACTIONS = "couchbaseTransactions"; + /** * The name for the bean that stores custom mapping between repositories and their backing couchbaseOperations. */ @@ -59,4 +61,7 @@ public class BeanNames { * The name for the bean that will handle reactive audit trail marking of entities. */ public static final String REACTIVE_COUCHBASE_AUDITING_HANDLER = "reactiveCouchbaseAuditingHandler"; + + public static final String COUCHBASE_CLIENT_FACTORY = "couchbaseClientFactory"; + } diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java new file mode 100644 index 000000000..10388e290 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -0,0 +1,186 @@ +package org.springframework.data.couchbase.core; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; +import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; + +import com.couchbase.client.core.error.CouchbaseException; +import org.springframework.util.ClassUtils; + +import java.util.Map; +import java.util.Set; + +public abstract class AbstractTemplateSupport { + + final ReactiveCouchbaseTemplate template; + final CouchbaseConverter converter; + final MappingContext, CouchbasePersistentProperty> mappingContext; + final TranslationService translationService; + ApplicationContext applicationContext; + static final Logger LOG = LoggerFactory.getLogger(AbstractTemplateSupport.class); + + public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConverter converter, TranslationService translationService) { + this.template = template; + this.converter = converter; + this.mappingContext = converter.getMappingContext(); + this.translationService = translationService; + } + + abstract ReactiveCouchbaseTemplate getReactiveTemplate(); + + public T decodeEntityBase(String id, String source, long cas, Class entityClass, + String scope, String collection, TransactionResultHolder txResultHolder) { + + final CouchbaseDocument converted = new CouchbaseDocument(id); + converted.setId(id); + + // this is the entity class defined for the repository. It may not be the class of the document that was read + // we will reset it after reading the document + // + // This will fail for the case where: + // 1) The version is defined in the concrete class, but not in the abstract class; and + // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in + // the source is null. + // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, + // but that is a lot of work to do every time just for this very rare and avoidable case. + // TypeInformation typeToUse = typeMapper.readType(source, type); + + CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); + + if (persistentEntity == null) { // method could return a Long, Boolean, String etc. + // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left + // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a + // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. + // if this is a Collection or array, only the first element will be returned. + Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) + .getContent().entrySet(); + return (T) set.iterator().next().getValue(); + } + + // if possible, set the version property in the source so that if the constructor has a long version argument, + // it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure + // can be avoid by defining the argument as Long instead of long. + // persistentEntity is still the (possibly abstract) class specified in the repository definition + // it's possible that the abstract class does not have a version property, and this won't be able to set the version + if (cas != 0 && persistentEntity.getVersionProperty() != null) { + converted.put(persistentEntity.getVersionProperty().getName(), cas); + } + + // if the constructor has an argument that is long version, then construction will fail if the 'version' + // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. + // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) + T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); + final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); + + persistentEntity = couldBePersistentEntity(readEntity.getClass()); + + if (cas != 0 && persistentEntity.getVersionProperty() != null) { + accessor.setProperty(persistentEntity.getVersionProperty(), cas); + } + N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id, scope, collection); + return accessor.getBean(); + } + + CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { + if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { + return null; + } + return mappingContext.getPersistentEntity(entityClass); + } + + + + public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder) { + ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + + final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() + .getRequiredPersistentEntity(entity.getClass()); + + final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + if (idProperty != null) { + accessor.setProperty(idProperty, id); + } + + final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + if (versionProperty != null) { + accessor.setProperty(versionProperty, cas); + } + + final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty(); + if (transactionResultProperty != null) { + accessor.setProperty(transactionResultProperty, txResultHolder); + } + maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted)); + return (T) accessor.getBean(); + + } + + public Long getCas(final Object entity) { + final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + + long cas = 0; + if (versionProperty != null) { + Object casObject = accessor.getProperty(versionProperty); + if (casObject instanceof Number) { + cas = ((Number) casObject).longValue(); + } + } + return cas; + } + + public String getJavaNameForEntity(final Class clazz) { + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); + MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); + return info.getJavaType().getName(); + } + + ConvertingPropertyAccessor getPropertyAccessor(final T source) { + CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); + return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); + } + + public TransactionResultHolder getTxResultHolder(T source) { + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(source.getClass()); + final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty(); + if (transactionResultProperty == null) { + throw new CouchbaseException("the entity class " + source.getClass() + + " does not have a property required for transactions:\n\t@TransactionResult TransactionResultHolder txResultHolder"); + } + return getPropertyAccessor(source).getProperty(transactionResultProperty, TransactionResultHolder.class); + } + + public void maybeEmitEvent(CouchbaseMappingEvent event) { + if (canPublishEvent()) { + try { + this.applicationContext.publishEvent(event); + } catch (Exception e) { + LOG.warn("{} thrown during {}", e, event); + throw e; + } + } else { + LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext"); + } + + } + + private boolean canPublishEvent() { + return this.applicationContext != null; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index b86806abb..1c1ef0920 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -16,33 +16,19 @@ package org.springframework.data.couchbase.core; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertCallback; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; -import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; -import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.data.mapping.callback.EntityCallbacks; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Internal encode/decode support for CouchbaseTemplate. @@ -53,23 +39,15 @@ * @author Carlos Espinaco * @since 3.0 */ -class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSupport { - - private static final Logger LOG = LoggerFactory.getLogger(CouchbaseTemplateSupport.class); +class CouchbaseTemplateSupport extends AbstractTemplateSupport implements ApplicationContextAware, TemplateSupport { private final CouchbaseTemplate template; - private final CouchbaseConverter converter; - private final MappingContext, CouchbasePersistentProperty> mappingContext; - private final TranslationService translationService; private EntityCallbacks entityCallbacks; - private ApplicationContext applicationContext; public CouchbaseTemplateSupport(final CouchbaseTemplate template, final CouchbaseConverter converter, final TranslationService translationService) { + super(template.reactive(), converter, translationService); this.template = template; - this.converter = converter; - this.mappingContext = converter.getMappingContext(); - this.translationService = translationService; } @Override @@ -84,122 +62,20 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) { } @Override - public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection) { - final CouchbaseDocument converted = new CouchbaseDocument(id); - converted.setId(id); - - // this is the entity class defined for the repository. It may not be the class of the document that was read - // we will reset it after reading the document - // - // This will fail for the case where: - // 1) The version is defined in the concrete class, but not in the abstract class; and - // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in - // the source is null. - // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, - // but that is a lot of work to do every time just for this very rare and avoidable case. - // TypeInformation typeToUse = typeMapper.readType(source, type); - - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { // method could return a Long, Boolean, String etc. - // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left - // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a - // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. - // if this is a Collection or array, only the first element will be returned. - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - - // if possible, set the version property in the source so that if the constructor has a long version argument, - // it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure - // can be avoid by defining the argument as Long instead of long. - // persistentEntity is still the (possibly abstract) class specified in the repository definition - // it's possible that the abstract class does not have a version property, and this won't be able to set the version - if (cas != 0 && persistentEntity.getVersionProperty() != null) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); - } - - // if the constructor has an argument that is long version, then construction will fail if the 'version' - // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. - // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); - final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - - persistentEntity = couldBePersistentEntity(readEntity.getClass()); - - if (cas != 0 && persistentEntity.getVersionProperty() != null) { - accessor.setProperty(persistentEntity.getVersionProperty(), cas); - } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection); - return accessor.getBean(); - } - - CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { - if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { - return null; - } - return mappingContext.getPersistentEntity(entityClass); - } - - @Override - public Object applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { - Object returnValue; - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - if (versionProperty != null) { - accessor.setProperty(versionProperty, cas); - returnValue = accessor.getBean(); - } else { - returnValue = entity; - } - maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); - - return returnValue; - } - - @Override - public Object applyUpdatedId(final Object entity, Object id) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); - - if (idProperty != null) { - accessor.setProperty(idProperty, id); - return accessor.getBean(); - } - return entity; + ReactiveCouchbaseTemplate getReactiveTemplate() { + return template.reactive(); } @Override - public long getCas(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - long cas = 0; - if (versionProperty != null) { - Object casObject = accessor.getProperty(versionProperty); - if (casObject instanceof Number) { - cas = ((Number) casObject).longValue(); - } - } - return cas; + public T decodeEntity(String id, String source, long cas, Class entityClass, + String scope, String collection, TransactionResultHolder txHolder) { + return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder); } @Override - public String getJavaNameForEntity(final Class clazz) { - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); - MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); - return info.getJavaType().getName(); - } - - private ConvertingPropertyAccessor getPropertyAccessor(final T source) { - CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); + public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder) { + return applyResultBase(entity, converted, id, cas, txResultHolder); } @Override @@ -225,24 +101,6 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; } - public void maybeEmitEvent(CouchbaseMappingEvent event) { - if (canPublishEvent()) { - try { - this.applicationContext.publishEvent(event); - } catch (Exception e) { - LOG.warn("{} thrown during {}", e, event); - throw e; - } - } else { - LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext"); - } - - } - - private boolean canPublishEvent() { - return this.applicationContext != null; - } - protected T maybeCallBeforeConvert(T object, String collection) { if (entityCallbacks != null) { return entityCallbacks.callback(BeforeConvertCallback.class, object, collection); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java index 2aed295fb..58e9146ea 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java @@ -37,7 +37,8 @@ * * @author Christoph Strobl * @since 2.0 - */public interface ExecutableFindByAnalyticsOperation { + */ +public interface ExecutableFindByAnalyticsOperation { /** * Queries the analytics service. @@ -114,65 +115,23 @@ default Optional first() { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { - - /** - * Set the filter for the analytics query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - TerminatingFindByAnalytics matching(AnalyticsQuery query); - - } - /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * * @param options to use for execution */ @Override - FindByAnalyticsWithQuery withOptions(AnalyticsOptions options); - } - - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithOptions inCollection(String collection); - } - - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { - /** - * With a different scope - * - * @param scope the scope to use. - */ - @Override - FindByAnalyticsInCollection inScope(String scope); + TerminatingFindByAnalytics withOptions(AnalyticsOptions options); } @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { + interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { /** * Allows to override the default scan consistency. @@ -180,7 +139,7 @@ interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { * @param scanConsistency the custom scan consistency to use for this analytics query. */ @Deprecated - FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); + FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); } @@ -194,10 +153,22 @@ interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWit FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); } + interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { + + /** + * Set the filter for the analytics query to be used. + * + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + FindByAnalyticsWithConsistency matching(AnalyticsQuery query); + + } + /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { /** * Define the target type fields should be mapped to.
@@ -207,9 +178,39 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistenc * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithConsistency as(Class returnType); + FindByAnalyticsWithQuery as(Class returnType); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByAnalyticsWithProjection inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } - interface ExecutableFindByAnalytics extends FindByAnalyticsWithProjection {} + interface ExecutableFindByAnalytics extends FindByAnalyticsInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java index ad88bc4de..c90fe410a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -83,7 +83,7 @@ public List all() { } @Override - public TerminatingFindByAnalytics matching(final AnalyticsQuery query) { + public FindByAnalyticsWithConsistency matching(final AnalyticsQuery query) { return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); } @@ -102,7 +102,7 @@ public FindByAnalyticsInCollection inScope(final String scope) { } @Override - public FindByAnalyticsWithConsistency inCollection(final String collection) { + public FindByAnalyticsWithProjection inCollection(final String collection) { return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); } @@ -121,7 +121,7 @@ public FindByAnalyticsWithConsistency withConsistency(final AnalyticsScanCons } @Override - public FindByAnalyticsWithConsistency as(final Class returnType) { + public FindByAnalyticsWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index 9cb60eeb7..7dec07692 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -18,14 +18,16 @@ import java.time.Duration; import java.util.Collection; -import org.springframework.data.couchbase.core.support.OneAndAllId; import org.springframework.data.couchbase.core.support.InCollection; +import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; -import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; -import org.springframework.data.couchbase.core.support.WithExpiry; +import com.couchbase.transactions.AttemptContextReactive; /** * Get Operations @@ -82,19 +84,58 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } + interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + @Override + FindByIdWithOptions project(String... fields); + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + + /** + * Provide attempt context + * + * @param the entity type to use for the results + */ + interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { + /** + * Finds the distinct values for a specified {@literal field} across a single collection + * + * @param txCtx Must not be {@literal null}. + * @return new instance of {@link ExecutableFindById}. + * @throws IllegalArgumentException if field is {@literal null}. + */ + @Override + FindByIdWithProjection transaction(AttemptContextReactive txCtx); + } + + interface FindByIdTxOrNot extends FindByIdWithExpiry, FindByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdWithOptions, InCollection { + interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdWithOptions inCollection(String collection); + FindByIdTxOrNot inCollection(String collection); } /** @@ -112,31 +153,11 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } - interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - @Override - FindByIdInScope project(String... fields); - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type to use for the results */ - interface ExecutableFindById extends FindByIdWithExpiry {} + interface ExecutableFindById extends FindByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 38cf2716b..4d16ffb00 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -24,6 +24,7 @@ import org.springframework.util.Assert; import com.couchbase.client.java.kv.GetOptions; +import com.couchbase.transactions.AttemptContextReactive; public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOperation { @@ -35,7 +36,7 @@ public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOpe @Override public ExecutableFindById findById(Class domainType) { - return new ExecutableFindByIdSupport<>(template, domainType, null, null, null, null, null); + return new ExecutableFindByIdSupport<>(template, domainType, null, null, null, null, null, null); } static class ExecutableFindByIdSupport implements ExecutableFindById { @@ -47,10 +48,11 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final GetOptions options; private final List fields; private final Duration expiry; + private final AttemptContextReactive txCtx; private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, - GetOptions options, List fields, Duration expiry) { + GetOptions options, List fields, Duration expiry, AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -58,8 +60,9 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { this.options = options; this.fields = fields; this.expiry = expiry; + this.txCtx = txCtx; this.reactiveSupport = new ReactiveFindByIdSupport<>(template.reactive(), domainType, scope, collection, options, - fields, expiry, new NonReactiveSupportWrapper(template.support())); + fields, expiry, txCtx, new NonReactiveSupportWrapper(template.support())); } @Override @@ -75,29 +78,35 @@ public Collection all(final Collection ids) { @Override public TerminatingFindById withOptions(final GetOptions options) { Assert.notNull(options, "Options must not be null."); - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } @Override - public FindByIdWithOptions inCollection(final String collection) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); + public FindByIdTxOrNot inCollection(final String collection) { + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } @Override public FindByIdInCollection inScope(final String scope) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } @Override public FindByIdInScope project(String... fields) { Assert.notEmpty(fields, "Fields must not be null."); - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), + expiry, txCtx); } @Override public FindByIdWithProjection withExpiry(final Duration expiry) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, - expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); + } + + @Override + public FindByIdWithExpiry transaction(AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index f147dda67..245cbaecc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -29,10 +29,12 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import org.springframework.lang.Nullable; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.transactions.AttemptContextReactive; /** * Query Operations @@ -128,42 +130,12 @@ default Optional first() { } - /** - * Fluent methods to specify the query - * - * @param the entity type to use for the results. - */ - interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { - - /** - * Set the filter for the query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - @Override - TerminatingFindByQuery matching(Query query); - - /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. - * - * @param criteria must not be {@literal null}. - * @return new instance of {@link ExecutableFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. - */ - @Override - default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - - } - /** * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { + interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { /** * Fluent method to specify options to use for execution * @@ -174,66 +146,86 @@ interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOp } /** - * Fluent method to specify the collection. - * + * To be removed at the next major release. use WithConsistency instead + * * @param the entity type to use for the results. */ - interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryWithOptions { + /** - * With a different collection + * Allows to override the default scan consistency. * - * @param collection the collection to use. + * @param scanConsistency the custom scan consistency to use for this query. */ - @Override - FindByQueryWithOptions inCollection(String collection); + @Deprecated + FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify the scope. + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. * * @param the entity type to use for the results. */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + /** - * With a different scope + * Allows to override the default scan consistency. * - * @param scope the scope to use. + * @param scanConsistency the custom scan consistency to use for this query. */ @Override - FindByQueryInCollection inScope(String scope); + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } /** - * To be removed at the next major release. use WithConsistency instead - * + * Fluent method to specify transaction + * * @param the entity type to use for the results. */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryInScope { + interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { /** - * Allows to override the default scan consistency. + * Finds the distinct values for a specified {@literal field} across a single collection * - * @param scanConsistency the custom scan consistency to use for this query. + * @param txCtx Must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. */ - @Deprecated - FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); + @Override + TerminatingFindByQuery transaction(AttemptContextReactive txCtx); } + interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} + /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * Fluent methods to specify the query * * @param the entity type to use for the results. */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { /** - * Allows to override the default scan consistency. + * Set the filter for the query to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ @Override - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + FindByQueryTxOrNot matching(Query query); + + /** + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * + * @param criteria must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. + */ + @Override + default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** @@ -241,7 +233,7 @@ interface FindByQueryWithConsistency extends FindByQueryConsistentWith, Wi * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithConsistency { + interface FindByQueryWithProjection extends FindByQueryWithQuery { /** * Define the target type fields should be mapped to.
@@ -251,7 +243,7 @@ interface FindByQueryWithProjection extends FindByQueryWithConsistency { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithConsistency as(Class returnType); + FindByQueryWithQuery as(Class returnType); } /** @@ -287,7 +279,37 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @throws IllegalArgumentException if field is {@literal null}. */ @Override - FindByQueryWithProjection distinct(String[] distinctFields); + FindByQueryWithProjecting distinct(String[] distinctFields); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByQueryWithDistinct inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByQueryInScope extends FindByQueryInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByQueryInCollection inScope(String scope); } /** @@ -295,6 +317,6 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * * @param the entity type to use for the results */ - interface ExecutableFindByQuery extends FindByQueryWithDistinct {} + interface ExecutableFindByQuery extends FindByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index cffdd5716..d06d522ec 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -24,6 +24,7 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.transactions.AttemptContextReactive; /** * {@link ExecutableFindByQueryOperation} implementations for Couchbase. @@ -44,7 +45,7 @@ public ExecutableFindByQueryOperationSupport(final CouchbaseTemplate template) { @Override public ExecutableFindByQuery findByQuery(final Class domainType) { return new ExecutableFindByQuerySupport(template, domainType, domainType, ALL_QUERY, null, null, null, null, - null, null); + null, null, null); } static class ExecutableFindByQuerySupport implements ExecutableFindByQuery { @@ -60,16 +61,18 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final QueryOptions options; private final String[] distinctFields; private final String[] fields; + private final AttemptContextReactive txCtx; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, - final QueryOptions options, final String[] distinctFields, final String[] fields) { + final QueryOptions options, final String[] distinctFields, final String[] fields, + final AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.returnType = returnType; this.query = query; this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, returnType, query, - scanConsistency, scope, collection, options, distinctFields, fields, + scanConsistency, scope, collection, options, distinctFields, fields, txCtx, new NonReactiveSupportWrapper(template.support())); this.scanConsistency = scanConsistency; this.scope = scope; @@ -77,6 +80,7 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery this.options = options; this.distinctFields = distinctFields; this.fields = fields; + this.txCtx = txCtx; } @Override @@ -95,7 +99,7 @@ public List all() { } @Override - public TerminatingFindByQuery matching(final Query query) { + public FindByQueryTxOrNot matching(final Query query) { QueryScanConsistency scanCons; if (query.getScanConsistency() != null) { scanCons = query.getScanConsistency(); @@ -103,27 +107,27 @@ public TerminatingFindByQuery matching(final Query query) { scanCons = scanConsistency; } return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields, fields); + options, distinctFields, fields, txCtx); } @Override @Deprecated public FindByQueryInScope consistentWith(final QueryScanConsistency scanConsistency) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override public FindByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override - public FindByQueryWithConsistency as(final Class returnType) { + public FindByQueryWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override @@ -131,11 +135,11 @@ public FindByQueryWithProjection project(String[] fields) { Assert.notNull(fields, "Fields must not be null"); Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override - public FindByQueryWithProjection distinct(final String[] distinctFields) { + public FindByQueryWithProjecting distinct(final String[] distinctFields) { Assert.notNull(distinctFields, "distinctFields must not be null"); Assert.isNull(fields, "only one of project(fields) and distinct(distinctFields) can be specified"); // Coming from an annotation, this cannot be null. @@ -143,7 +147,14 @@ public FindByQueryWithProjection distinct(final String[] distinctFields) { // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields, fields); + collection, options, dFields, fields, txCtx); + } + + @Override + public FindByQueryWithDistinct transaction(AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null!"); + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields, txCtx); } @Override @@ -169,19 +180,19 @@ public boolean exists() { public TerminatingFindByQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override public FindByQueryInCollection inScope(final String scope) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override - public FindByQueryWithConsistency inCollection(final String collection) { + public FindByQueryWithDistinct inCollection(final String collection) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index 0465d5022..48f4ba8e6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -24,11 +24,13 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; /** * Insert Operations @@ -73,8 +75,7 @@ interface TerminatingInsertById extends OneAndAllEntity { * * @param the entity type to use. */ - interface InsertByIdWithOptions - extends TerminatingInsertById, WithInsertOptions { + interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertOptions { /** * Fluent method to specify options to use for execution. * @@ -84,19 +85,42 @@ interface InsertByIdWithOptions TerminatingInsertById withOptions(InsertOptions options); } + interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { + + @Override + InsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + + interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { + @Override + InsertByIdWithExpiry transaction(AttemptContextReactive txCtx); + } + + interface InsertByIdTxOrNot extends InsertByIdWithExpiry, InsertByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { + interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdWithOptions inCollection(String collection); + InsertByIdTxOrNot inCollection(String collection); } /** @@ -114,27 +138,11 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } - interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { - - @Override - InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ExecutableInsertById extends InsertByIdWithExpiry {} + interface ExecutableInsertById extends InsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index 8eb4b99f5..a03d481d1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -25,6 +25,7 @@ import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; public class ExecutableInsertByIdOperationSupport implements ExecutableInsertByIdOperation { @@ -38,7 +39,7 @@ public ExecutableInsertByIdOperationSupport(final CouchbaseTemplate template) { public ExecutableInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableInsertByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ExecutableInsertByIdSupport implements ExecutableInsertById { @@ -52,11 +53,12 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final AttemptContextReactive txCtx; private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final DurabilityLevel durabilityLevel, final Duration expiry, final AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -66,8 +68,10 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; this.reactiveSupport = new ReactiveInsertByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, + new NonReactiveSupportWrapper(template.support())); } @Override @@ -84,26 +88,26 @@ public Collection all(Collection objects) { public TerminatingInsertById withOptions(final InsertOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override public InsertByIdInCollection inScope(final String scope) { return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override - public InsertByIdWithOptions inCollection(final String collection) { + public InsertByIdTxOrNot inCollection(final String collection) { return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override @@ -111,14 +115,21 @@ public InsertByIdInScope withDurability(final PersistTo persistTo, final Repl Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override public InsertByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); + } + + @Override + public InsertByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 02381669e..1c650d9ce 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -21,13 +21,16 @@ import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithCas; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; /** * Remove Operations on KV service. @@ -85,17 +88,39 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } + interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { + + @Override + RemoveByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability, WithCas { + @Override + RemoveByIdWithDurability withCas(Long cas); + } + + interface RemoveByIdWithTransaction extends TerminatingRemoveById, WithTransaction { + @Override + TerminatingRemoveById transaction(AttemptContextReactive txCtx); + } + + interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} + /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { + interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - RemoveByIdWithOptions inCollection(String collection); + RemoveByIdTxOrNot inCollection(String collection); } /** @@ -111,24 +136,9 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { RemoveByIdInCollection inScope(String scope); } - interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { - - @Override - RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability { - - RemoveByIdWithDurability withCas(Long cas); - } - /** * Provides methods for constructing remove operations in a fluent way. */ - interface ExecutableRemoveById extends RemoveByIdWithCas {} + interface ExecutableRemoveById extends RemoveByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index e0721ce5a..0176e9493 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -25,6 +25,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByIdOperation { @@ -43,7 +44,7 @@ public ExecutableRemoveById removeById() { @Override public ExecutableRemoveById removeById(Class domainType) { return new ExecutableRemoveByIdSupport(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { @@ -57,11 +58,12 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; + private final AttemptContextReactive txCtx; private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas) { + final DurabilityLevel durabilityLevel, Long cas, AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -71,8 +73,9 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdSupport(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, cas); + options, persistTo, replicateTo, durabilityLevel, cas, txCtx); this.cas = cas; + this.txCtx = txCtx; } @Override @@ -86,16 +89,16 @@ public List all(final Collection ids) { } @Override - public RemoveByIdWithOptions inCollection(final String collection) { + public RemoveByIdTxOrNot inCollection(final String collection) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override @@ -103,27 +106,34 @@ public RemoveByIdInScope withDurability(final PersistTo persistTo, final Replica Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public TerminatingRemoveById withOptions(final RemoveOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdInCollection inScope(final String scope) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdWithDurability withCas(Long cas) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } + + @Override + public RemoveByIdWithCas transaction(AttemptContextReactive txCtx) { + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, cas, txCtx); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index a6bfdf0cf..ede240095 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -24,9 +24,11 @@ import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.transactions.AttemptContextReactive; /** * RemoveBy Query Operations @@ -56,32 +58,58 @@ interface TerminatingRemoveByQuery { } /** - * Fluent methods to specify the query + * Fluent method to specify options. * - * @param the entity type. + * @param the entity type to use for the results. */ - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { + interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + TerminatingRemoveByQuery withOptions(QueryOptions options); + } - TerminatingRemoveByQuery matching(Query query); + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { - default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } + @Deprecated + RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith/*, WithConsistency */{ + //@Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify options. + * Fluent method to specify the transaction * * @param the entity type to use for the results. */ - interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { - /** - * Fluent method to specify options to use for execution - * - * @param options to use for execution - */ - RemoveByQueryWithQuery withOptions(QueryOptions options); + interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { + @Override + TerminatingRemoveByQuery transaction(AttemptContextReactive txCtx); + } + + interface RemoveByQueryWithTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} + + /** + * Fluent methods to specify the query + * + * @param the entity type. + */ + interface RemoveByQueryWithQuery extends RemoveByQueryWithTxOrNot, WithQuery { + + RemoveByQueryWithTxOrNot matching(Query query); + + default RemoveByQueryWithTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** @@ -89,13 +117,13 @@ interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQue * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithOptions inCollection(String collection); + RemoveByQueryWithQuery inCollection(String collection); } /** @@ -112,25 +140,11 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { - - @Deprecated - RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - @Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ExecutableRemoveByQuery extends RemoveByQueryWithConsistency {} + interface ExecutableRemoveByQuery extends RemoveByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index a97c62ba3..3e9497830 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -23,6 +23,7 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.transactions.AttemptContextReactive; public class ExecutableRemoveByQueryOperationSupport implements ExecutableRemoveByQueryOperation { @@ -36,7 +37,7 @@ public ExecutableRemoveByQueryOperationSupport(final CouchbaseTemplate template) @Override public ExecutableRemoveByQuery removeByQuery(Class domainType) { - return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null); + return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null, null); } static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuery { @@ -49,18 +50,21 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final String scope; private final String collection; private final QueryOptions options; + private final AttemptContextReactive txCtx; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.query = query; this.reactiveSupport = new ReactiveRemoveByQuerySupport<>(template.reactive(), domainType, query, scanConsistency, - scope, collection, options); + scope, collection, options, txCtx); this.scanConsistency = scanConsistency; this.scope = scope; this.collection = collection; this.options = options; + this.txCtx = txCtx; } @Override @@ -69,41 +73,47 @@ public List all() { } @Override - public TerminatingRemoveByQuery matching(final Query query) { + public RemoveByQueryWithTxOrNot matching(final Query query) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override @Deprecated public RemoveByQueryInScope consistentWith(final QueryScanConsistency scanConsistency) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override - public RemoveByQueryWithConsistency inCollection(final String collection) { + public RemoveByQueryWithQuery inCollection(final String collection) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryInCollection inScope(final String scope) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); + } + + @Override + public TerminatingRemoveByQuery transaction(final AttemptContextReactive txCtx) { + return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, + options, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index 51ce8e98b..f16d4e623 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -24,11 +24,13 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; /** * Replace Operations @@ -83,19 +85,40 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } + interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { + @Override + ReplaceByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + ReplaceByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + @Override + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + + interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { + @Override + TerminatingReplaceById transaction(AttemptContextReactive txCtx); + } + + interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdWithOptions inCollection(String collection); + ReplaceByIdTxOrNot inCollection(String collection); } /** @@ -113,24 +136,11 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope { ReplaceByIdInCollection inScope(String scope); } - interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { - @Override - ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); - @Override - ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - @Override - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ExecutableReplaceById extends ReplaceByIdWithExpiry {} + interface ExecutableReplaceById extends ReplaceByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index ef47eb94a..4427df94a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -25,6 +25,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; public class ExecutableReplaceByIdOperationSupport implements ExecutableReplaceByIdOperation { @@ -38,7 +39,7 @@ public ExecutableReplaceByIdOperationSupport(final CouchbaseTemplate template) { public ExecutableReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableReplaceByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ExecutableReplaceByIdSupport implements ExecutableReplaceById { @@ -52,11 +53,12 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final AttemptContextReactive txCtx; private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final DurabilityLevel durabilityLevel, final Duration expiry, final AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -66,8 +68,10 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; this.reactiveSupport = new ReactiveReplaceByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, + new NonReactiveSupportWrapper(template.support())); } @Override @@ -81,16 +85,16 @@ public Collection all(Collection objects) { } @Override - public ReplaceByIdWithOptions inCollection(final String collection) { + public ReplaceByIdTxOrNot inCollection(final String collection) { return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override public ReplaceByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override @@ -98,27 +102,34 @@ public ReplaceByIdInScope withDurability(final PersistTo persistTo, final Rep Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override public ReplaceByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); + } + + @Override + public ReplaceByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, + replicateTo, durabilityLevel, expiry, txCtx); } @Override public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override public ReplaceByIdInCollection inScope(final String scope) { return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java index 0831f8eb4..56f93d02a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java @@ -83,19 +83,32 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } + interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { + @Override + UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithOptions inCollection(String collection); + UpsertByIdWithExpiry inCollection(String collection); } /** @@ -113,25 +126,11 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope { UpsertByIdInCollection inScope(String scope); } - interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { - @Override - UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ExecutableUpsertById extends UpsertByIdWithExpiry {} + interface ExecutableUpsertById extends UpsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java index 61b7a3945..7dd548b73 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java @@ -94,7 +94,7 @@ public UpsertByIdInCollection inScope(final String scope) { } @Override - public UpsertByIdWithOptions inCollection(final String collection) { + public UpsertByIdWithExpiry inCollection(final String collection) { return new ExecutableUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry); } diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index bee19a4a8..1a41ee846 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -19,6 +19,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. @@ -40,19 +41,15 @@ public Mono encodeEntity(Object entityToEncode) { } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, - String collection) { - return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder) { + return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope , collection, txResultHolder)); } @Override - public Mono applyUpdatedCas(Object entity, CouchbaseDocument converted, long cas) { - return Mono.fromSupplier(() -> support.applyUpdatedCas(entity, converted, cas)); - } - - @Override - public Mono applyUpdatedId(Object entity, Object id) { - return Mono.fromSupplier(() -> support.applyUpdatedId(entity, id)); + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder) { + return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder)); } @Override @@ -66,7 +63,7 @@ public String getJavaNameForEntity(Class clazz) { } @Override - public void maybeEmitEvent(CouchbaseMappingEvent event) { - support.maybeEmitEvent(event); + public TransactionResultHolder getTxResultHolder(T source) { + return support.getTxResultHolder(source); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 5cf321b45..5a61b1a6a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -18,32 +18,19 @@ import reactor.core.publisher.Mono; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; -import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; -import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -53,23 +40,16 @@ * @author Carlos Espinaco * @since 4.2 */ -class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, ReactiveTemplateSupport { - - private static final Logger LOG = LoggerFactory.getLogger(ReactiveCouchbaseTemplateSupport.class); +class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport + implements ApplicationContextAware, ReactiveTemplateSupport { private final ReactiveCouchbaseTemplate template; - private final CouchbaseConverter converter; - private final MappingContext, CouchbasePersistentProperty> mappingContext; - private final TranslationService translationService; private ReactiveEntityCallbacks reactiveEntityCallbacks; - private ApplicationContext applicationContext; public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template, final CouchbaseConverter converter, final TranslationService translationService) { + super(template, converter, translationService); this.template = template; - this.converter = converter; - this.mappingContext = converter.getMappingContext(); - this.translationService = translationService; } @Override @@ -84,109 +64,21 @@ public Mono encodeEntity(final Object entityToEncode) { } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, - String collection) { - return Mono.fromSupplier(() -> { - final CouchbaseDocument converted = new CouchbaseDocument(id); - converted.setId(id); - - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { // method could return a Long, Boolean, String etc. - // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left - // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a - // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. - // if this is a Collection or array, only the first element will be returned. - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - - if (cas != 0 && persistentEntity.getVersionProperty() != null) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); - } - - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); - final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - - if (persistentEntity.getVersionProperty() != null) { - accessor.setProperty(persistentEntity.getVersionProperty(), cas); - } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id, scope, collection); - return accessor.getBean(); - }); - } - - CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { - if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { - return null; - } - return mappingContext.getPersistentEntity(entityClass); - } - - @Override - public Mono applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { - return Mono.fromSupplier(() -> { - Object returnValue; - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - if (versionProperty != null) { - accessor.setProperty(versionProperty, cas); - returnValue = accessor.getBean(); - } else { - returnValue = entity; - } - maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); - return returnValue; - }); - } - - @Override - public Mono applyUpdatedId(final Object entity, Object id) { - return Mono.fromSupplier(() -> { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); - - if (idProperty != null) { - accessor.setProperty(idProperty, id); - return accessor.getBean(); - } - return entity; - }); + ReactiveCouchbaseTemplate getReactiveTemplate() { + return template; } @Override - public Long getCas(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - long cas = 0; - if (versionProperty != null) { - Object casObject = accessor.getProperty(versionProperty); - if (casObject instanceof Number) { - cas = ((Number) casObject).longValue(); - } - } - return cas; + public Mono decodeEntity(String id, String source, long cas, Class entityClass, + String scope, String collection, TransactionResultHolder txResultHolder) { + return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder)); } @Override - public String getJavaNameForEntity(final Class clazz) { - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); - MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); - return info.getJavaType().getName(); - } + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder) { + return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder)); - private ConvertingPropertyAccessor getPropertyAccessor(final T source) { - CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); } @Override @@ -212,24 +104,6 @@ public void setReactiveEntityCallbacks(ReactiveEntityCallbacks reactiveEntityCal this.reactiveEntityCallbacks = reactiveEntityCallbacks; } - public void maybeEmitEvent(CouchbaseMappingEvent event) { - if (canPublishEvent()) { - try { - this.applicationContext.publishEvent(event); - } catch (Exception e) { - LOG.warn("{} thrown during {}", e, event); - throw e; - } - } else { - LOG.info("maybeEmitEvent called, but ReactiveCouchbaseTemplate not initialized with applicationContext"); - } - - } - - private boolean canPublishEvent() { - return this.applicationContext != null; - } - protected Mono maybeCallBeforeConvert(T object, String collection) { if (reactiveEntityCallbacks != null) { return reactiveEntityCallbacks.callback(ReactiveBeforeConvertCallback.class, object, collection); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index 56b642820..cf7c116ce 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -71,7 +71,7 @@ static class ReactiveExistsByIdSupport implements ReactiveExistsById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, null, domainType); LOG.trace("existsById {}", pArgs); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) @@ -102,7 +102,7 @@ public ExistsByIdWithOptions inCollection(final String collection) { } @Override - public TerminatingExistsById withOptions(final ExistsOptions options) { + public ExistsByIdInScope withOptions(final ExistsOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveExistsByIdSupport(template, domainType, scope, collection, options); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java index 1d661b302..2d9f1251b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java @@ -88,24 +88,12 @@ interface TerminatingFindByAnalytics extends OneAndAllReactive { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { - - /** - * Set the filter for the analytics query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - TerminatingFindByAnalytics matching(AnalyticsQuery query); - - } - /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * @@ -115,65 +103,47 @@ interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, Wit TerminatingFindByAnalytics withOptions(AnalyticsOptions options); } - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithOptions inCollection(String collection); - } + @Deprecated + interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { /** - * With a different scope + * Allows to override the default scan consistency. * - * @param scope the scope to use. + * @param scanConsistency the custom scan consistency to use for this analytics query. */ - @Override - FindByAnalyticsInCollection inScope(String scope); + @Deprecated + FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); + } - @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { + interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWith, WithAnalyticsConsistency { /** * Allows to override the default scan consistency. * * @param scanConsistency the custom scan consistency to use for this analytics query. */ - @Deprecated - FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); + @Override + FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); } - interface FindByAnalyticsWithConsistency extends FindByAnalyticsInScope, WithAnalyticsConsistency { + interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { /** - * Allows to override the default scan consistency. + * Set the filter for the analytics query to be used. * - * @param scanConsistency the custom scan consistency to use for this analytics query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ - @Override - FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scanConsistency); + FindByAnalyticsWithConsistency matching(AnalyticsQuery query); } /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { /** * Define the target type fields should be mapped to.
@@ -183,9 +153,39 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistenc * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithConsistency as(Class returnType); + FindByAnalyticsWithQuery as(Class returnType); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByAnalyticsWithProjection inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } - interface ReactiveFindByAnalytics extends FindByAnalyticsWithProjection, FindByAnalyticsConsistentWith {} + interface ReactiveFindByAnalytics extends FindByAnalyticsInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index b46a81f53..ccd5ac529 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -70,7 +70,7 @@ static class ReactiveFindByAnalyticsSupport implements ReactiveFindByAnalytic } @Override - public TerminatingFindByAnalytics matching(AnalyticsQuery query) { + public FindByAnalyticsWithConsistency matching(AnalyticsQuery query) { return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); } @@ -89,7 +89,7 @@ public FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scan } @Override - public FindByAnalyticsWithConsistency as(final Class returnType) { + public FindByAnalyticsWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); @@ -133,7 +133,7 @@ public Flux all() { cas = row.getLong(TemplateUtils.SELECT_CAS); row.removeKey(TemplateUtils.SELECT_ID); row.removeKey(TemplateUtils.SELECT_CAS); - return support.decodeEntity(id, row.toString(), cas, returnType, null, null); + return support.decodeEntity(id, row.toString(), cas, returnType, null, null, null); }); }); } @@ -173,7 +173,7 @@ public FindByAnalyticsInCollection inScope(final String scope) { } @Override - public FindByAnalyticsWithConsistency inCollection(final String collection) { + public FindByAnalyticsWithProjection inCollection(final String collection) { return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index 5e9983b0f..661659baa 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -27,11 +27,13 @@ import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; +import com.couchbase.transactions.AttemptContextReactive; /** - * Get Operations + * Get Operations - method/interface chaining is from the bottom up. * * @author Christoph Strobl * @since 2.0 @@ -67,7 +69,21 @@ interface TerminatingFindById extends OneAndAllIdReactive { * @return the list of found entities. */ Flux all(Collection ids); + } + /** + * Provide transaction + * + * @param the entity type to use for the results + */ + interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { + /** + * Provide transaction + * + * @param txCtx + * @return + */ + TerminatingFindById transaction(AttemptContextReactive txCtx); } /** @@ -85,19 +101,45 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } + interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + FindByIdWithOptions project(String... fields); + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + + /** + * Interface to that can produce either transactional or non-transactional operations. + * + * @param the entity type to use for the results. + */ + interface FindByIdTxOrNot extends FindByIdWithTransaction, FindByIdWithExpiry {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdWithOptions, InCollection { + interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdWithOptions inCollection(String collection); + FindByIdTxOrNot inCollection(String collection); } /** @@ -115,32 +157,11 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } - interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { - - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - FindByIdInCollection project(String... fields); - - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - /** * Provides methods for constructing query operations in a fluent way. * - * @param the entity type to use for the results + * @param the entity type. */ - interface ReactiveFindById extends FindByIdWithExpiry {} + interface ReactiveFindById extends FindByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 347054579..0fcb1b928 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.util.Assert; import com.couchbase.client.core.error.DocumentNotFoundException; @@ -37,6 +38,7 @@ import com.couchbase.client.java.codec.RawJsonTranscoder; import com.couchbase.client.java.kv.GetAndTouchOptions; import com.couchbase.client.java.kv.GetOptions; +import com.couchbase.transactions.AttemptContextReactive; public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperation { @@ -49,7 +51,7 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati @Override public ReactiveFindById findById(Class domainType) { - return new ReactiveFindByIdSupport<>(template, domainType, null, null, null, null, null, template.support()); + return new ReactiveFindByIdSupport<>(template, domainType, null, null, null, null, null, null, template.support()); } static class ReactiveFindByIdSupport implements ReactiveFindById { @@ -60,11 +62,13 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { private final String collection; private final CommonOptions options; private final List fields; + private final AttemptContextReactive txCtx; private final ReactiveTemplateSupport support; private final Duration expiry; ReactiveFindByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, - CommonOptions options, List fields, Duration expiry, ReactiveTemplateSupport support) { + CommonOptions options, List fields, Duration expiry, AttemptContextReactive txCtx, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -72,6 +76,7 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { this.options = options; this.fields = fields; this.expiry = expiry; + this.txCtx = txCtx; this.support = support; } @@ -79,33 +84,42 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { public Mono one(final String id) { CommonOptions gOptions = initGetOptions(); - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, txCtx, domainType); LOG.trace("findById {}", pArgs); - - return Mono.just(id).flatMap(docId -> { - ReactiveCollection reactive = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive(); + ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive(); + Mono reactiveEntity; + if (pArgs.getCtx() == null) { if (pArgs.getOptions() instanceof GetAndTouchOptions) { - return reactive.getAndTouch(docId, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()); + reactiveEntity = rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()).flatMap( + result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); + } else { + reactiveEntity = rc.get(id, (GetOptions) pArgs.getOptions()).flatMap( + result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType,pArgs.getScope(), pArgs.getCollection(), null)); + } + } else { + reactiveEntity = pArgs.getCtx().get(rc, id).flatMap(result -> support.decodeEntity(id, + result.contentAsObject().toString(), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result))); + } + return reactiveEntity.onErrorResume(throwable -> { + if (throwable instanceof DocumentNotFoundException) { + return Mono.empty(); + } + return Mono.error(throwable); + }).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { - return reactive.get(docId, (GetOptions) pArgs.getOptions()); + return throwable; } - }).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, - pArgs.getScope(), pArgs.getCollection())).onErrorResume(throwable -> { - if (throwable instanceof RuntimeException) { - if (throwable instanceof DocumentNotFoundException) { - return Mono.empty(); - } - } - return Mono.error(throwable); - }).onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + }); + } + /* + private TransactionGetOptions buildTranasactionOptions(ReplaceOptions buildOptions) { + return OptionsBuilder.buildTransactionGetOptions(buildOptions); + } + */ @Override public Flux all(final Collection ids) { @@ -113,31 +127,42 @@ public Flux all(final Collection ids) { } @Override - public TerminatingFindById withOptions(final GetOptions options) { + public FindByIdInScope withOptions(final GetOptions options) { Assert.notNull(options, "Options must not be null."); - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } @Override - public FindByIdWithOptions inCollection(final String collection) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + public FindByIdInCollection inCollection(final String collection) { + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } @Override public FindByIdInCollection inScope(final String scope) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } @Override - public FindByIdInScope project(String... fields) { + public FindByIdWithOptions project(String... fields) { Assert.notNull(fields, "Fields must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), - expiry, support); + expiry, txCtx, support); } @Override public FindByIdWithProjection withExpiry(final Duration expiry) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); + } + + @Override + public FindByIdWithProjection transaction(AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null"); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } private CommonOptions initGetOptions() { @@ -172,6 +197,7 @@ private Duration expiryToUse() { } return expiryToUse; } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index 9a839ed7a..c97aa9962 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -28,9 +28,11 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.transactions.AttemptContextReactive; /** * ReactiveFindByQueryOperation
@@ -91,40 +93,12 @@ interface TerminatingFindByQuery extends OneAndAllReactive { } - /** - * Fluent methods to filter by query - * - * @param the entity type to use for the results. - */ - interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { - - /** - * Set the filter {@link Query} to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - TerminatingFindByQuery matching(Query query); - - /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. - * - * @param criteria must not be {@literal null}. - * @return new instance of {@link TerminatingFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. - */ - default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - - } - /** * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { + interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { /** * @param options options to use for execution */ @@ -132,54 +106,87 @@ interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOp } /** - * Fluent method to specify the collection - * + * To be removed at the next major release. use WithConsistency instead + * * @param the entity type to use for the results. */ - interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { - FindByQueryWithOptions inCollection(String collection); + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryWithOptions { + + /** + * Allows to override the default scan consistency. + * + * @param scanConsistency the custom scan consistency to use for this query. + */ + @Deprecated + FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify the scope + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. * * @param the entity type to use for the results. */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { - FindByQueryInCollection inScope(String scope); + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + + /** + * Allows to override the default scan consistency. + * + * @param scanConsistency the custom scan consistency to use for this query. + */ + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + } /** - * To be removed at the next major release. use WithConsistency instead + * Fluent method to add transactions * * @param the entity type to use for the results. */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryInScope { + interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { /** - * Allows to override the default scan consistency. + * Finds the distinct values for a specified {@literal field} across a single {@link } or view. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param txCtx Must not be {@literal null}. + * @return new instance of {@link ReactiveFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. */ - @Deprecated - FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - + TerminatingFindByQuery transaction(AttemptContextReactive txCtx); } /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * Fluent interface for operations with or without a transaction. * * @param the entity type to use for the results. */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} + + /** + * Fluent methods to filter by query + * + * @param the entity type to use for the results. + */ + interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { /** - * Allows to override the default scan consistency. + * Set the filter {@link Query} to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + FindByQueryTxOrNot matching(Query query); + + /** + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * + * @param criteria must not be {@literal null}. + * @return new instance of {@link TerminatingFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. + */ + default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } @@ -188,7 +195,7 @@ interface FindByQueryWithConsistency extends FindByQueryConsistentWith, Wi * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithConsistency { + interface FindByQueryWithProjection extends FindByQueryWithQuery { /** * Define the target type fields should be mapped to.
@@ -198,7 +205,7 @@ interface FindByQueryWithProjection extends FindByQueryWithConsistency { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithConsistency as(Class returnType); + FindByQueryWithQuery as(Class returnType); } /** @@ -233,7 +240,25 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @return new instance of {@link ReactiveFindByQuery}. * @throws IllegalArgumentException if field is {@literal null}. */ - FindByQueryWithProjection distinct(String[] distinctFields); + FindByQueryWithProjecting distinct(String[] distinctFields); + } + + /** + * Fluent method to specify the collection + * + * @param the entity type to use for the results. + */ + interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { + FindByQueryWithDistinct inCollection(String collection); + } + + /** + * Fluent method to specify the scope + * + * @param the entity type to use for the results. + */ + interface FindByQueryInScope extends FindByQueryInCollection, InScope { + FindByQueryInCollection inScope(String scope); } /** @@ -241,6 +266,6 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * * @param the entity type to use for the results */ - interface ReactiveFindByQuery extends FindByQueryWithDistinct {} + interface ReactiveFindByQuery extends FindByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index cfa3e0f34..341770666 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -20,15 +20,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionQueryOptions; /** * {@link ReactiveFindByQueryOperation} implementations for Couchbase. @@ -50,7 +55,7 @@ public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate templ @Override public ReactiveFindByQuery findByQuery(final Class domainType) { return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, null, null, null, null, null, - null, template.support()); + null, null, template.support()); } static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @@ -65,12 +70,13 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final String[] distinctFields; private final String[] fields; private final QueryOptions options; + private final AttemptContextReactive txCtx; private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, - final ReactiveTemplateSupport support) { + final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, + final AttemptContextReactive txCtx, final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); this.template = template; @@ -84,6 +90,7 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { this.distinctFields = distinctFields; this.fields = fields; this.support = support; + this.txCtx = txCtx; } @Override @@ -96,45 +103,45 @@ public FindByQueryWithQuery matching(Query query) { scanCons = scanConsistency; } return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields, fields, support); + options, distinctFields, fields, txCtx, support); } @Override - public TerminatingFindByQuery withOptions(final QueryOptions options) { + public FindByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override public FindByQueryInCollection inScope(final String scope) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override - public FindByQueryWithConsistency inCollection(final String collection) { + public FindByQueryWithDistinct inCollection(final String collection) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override @Deprecated - public FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency) { + public FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override public FindByQueryWithConsistency withConsistency(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } - public FindByQueryWithConsistency as(Class returnType) { + public FindByQueryWithProjecting as(Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override @@ -142,7 +149,7 @@ public FindByQueryWithProjection project(String[] fields) { Assert.notNull(fields, "Fields must not be null"); Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override @@ -154,7 +161,14 @@ public FindByQueryWithDistinct distinct(final String[] distinctFields) { // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields, fields, support); + collection, options, dFields, fields, txCtx, support); + } + + @Override + public FindByQueryWithTransaction transaction(AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null!"); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields, txCtx, support); } @Override @@ -169,15 +183,22 @@ public Mono first() { @Override public Flux all() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - Mono allResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return Flux.defer(() -> allResult.onErrorMap(throwable -> { + Mono allResult = null; + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); + if (pArgs.getCtx() == null) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + allResult = pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + allResult = pArgs.getScope() == null ? pArgs.getCtx().query(statement, opts) : txCtx.query(rs, statement, opts); + } + Mono finalAllResult = allResult; + return finalAllResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { @@ -202,8 +223,8 @@ public Flux all() { row.removeKey(TemplateUtils.SELECT_ID); row.removeKey(TemplateUtils.SELECT_CAS); } - return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection()); - })); + return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), null); + }); } public QueryOptions buildOptions(QueryOptions options) { @@ -211,17 +232,29 @@ public QueryOptions buildOptions(QueryOptions options) { return query.buildQueryOptions(options, qsc); } + private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { + TransactionQueryOptions opts = OptionsBuilder.buildTransactionQueryOptions(buildOptions(options)); + return opts; + } + @Override public Mono count() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - Mono countResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return Mono.defer(() -> countResult.onErrorMap(throwable -> { + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rc = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); + Mono countResult = null; + if (txCtx == null) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + countResult = pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rc.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + countResult = pArgs.getScope() == null ? txCtx.query(statement, opts) : txCtx.query(rc, statement, opts); + } + Mono finalCountResult = countResult; + return Mono.defer(() -> finalCountResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 0e1372b4f..e04237ed7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -72,13 +72,13 @@ public Mono any(final String id) { if (garOptions.build().transcoder() == null) { garOptions.transcoder(RawJsonTranscoder.INSTANCE); } - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, null, + domainType); LOG.trace("getAnyReplica {}", pArgs); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, - pArgs.getScope(), pArgs.getCollection())) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 6439879fe..7cde94e01 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -27,11 +27,13 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; /** * Insert Operations @@ -84,17 +86,43 @@ interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertO TerminatingInsertById withOptions(InsertOptions options); } + interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { + + @Override + InsertByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + + interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { + @Override + InsertByIdWithDurability transaction(AttemptContextReactive txCtx); + } + /** * Fluent method to specify the collection. */ - interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { + interface InsertByIdTxOrNot extends InsertByIdWithTransaction, InsertByIdWithExpiry {} + + /** + * Fluent method to specify the collection. + */ + interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdWithOptions inCollection(String collection); + InsertByIdTxOrNot inCollection(String collection); } /** @@ -110,27 +138,11 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } - interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { - - @Override - InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ReactiveInsertById extends InsertByIdWithExpiry {} + interface ReactiveInsertById extends InsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 0d319fb4a..728c217ff 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -23,15 +23,20 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionInsertOptions; public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { @@ -46,7 +51,7 @@ public ReactiveInsertByIdOperationSupport(final ReactiveCouchbaseTemplate templa public ReactiveInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveInsertByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null, template.support()); + DurabilityLevel.NONE, null, null, template.support()); } static class ReactiveInsertByIdSupport implements ReactiveInsertById { @@ -60,11 +65,13 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final AttemptContextReactive txCtx; private final ReactiveTemplateSupport support; ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, ReactiveTemplateSupport support) { + final DurabilityLevel durabilityLevel, Duration expiry, AttemptContextReactive txCtx, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -74,26 +81,37 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; this.support = support; } @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); LOG.trace("insertById {}", pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive(); + Mono reactiveEntity; + if (pArgs.getCtx() == null) { + reactiveEntity = support.encodeEntity(object) + .flatMap(converted -> rc + .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + } else { + reactiveEntity = support.encodeEntity(object) + .flatMap(converted -> pArgs.getCtx() + .insert(rc, converted.getId(), converted.getContent(), buildTxOptions(pArgs.getOptions(), converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), + new TransactionResultHolder(result)))); + } + + return reactiveEntity.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); } @Override @@ -105,30 +123,34 @@ public InsertOptions buildOptions(InsertOptions options, CouchbaseDocument doc) return OptionsBuilder.buildInsertOptions(options, persistTo, replicateTo, durabilityLevel, expiry, doc); } + private TransactionInsertOptions buildTxOptions(InsertOptions buildOptions, CouchbaseDocument doc) { + return OptionsBuilder.buildTxInsertOptions(buildOptions(buildOptions, doc)); + } + @Override public TerminatingInsertById withOptions(final InsertOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public InsertByIdInCollection inScope(final String scope) { return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override - public InsertByIdWithOptions inCollection(final String collection) { + public InsertByIdTxOrNot inCollection(final String collection) { return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override @@ -136,15 +158,23 @@ public InsertByIdInScope withDurability(final PersistTo persistTo, final Repl Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public InsertByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } + + @Override + public InsertByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, txCtx, support); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index a9abaa178..0cf7c5a48 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -25,11 +25,13 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; /** * Remove Operations on KV service. @@ -86,22 +88,42 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } + interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { + @Override + RemoveByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability { + + RemoveByIdWithDurability withCas(Long cas); + } + + interface RemoveByIdWithTransaction extends RemoveByIdWithCas, WithTransaction { + RemoveByIdWithCas transaction(AttemptContextReactive txCtx); + } + + interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} + /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { + interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByIdWithOptions inCollection(String collection); + RemoveByIdTxOrNot inCollection(String collection); } /** * Fluent method to specify the scope. */ - interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { + interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { /** * With a different scope * @@ -110,23 +132,9 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { RemoveByIdInCollection inScope(String scope); } - interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { - @Override - RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability { - - RemoveByIdWithDurability withCas(Long cas); - } - /** * Provides methods for constructing remove operations in a fluent way. */ - interface ReactiveRemoveById extends RemoveByIdWithCas {} + interface ReactiveRemoveById extends RemoveByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 547c561e5..a14eac74d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -19,6 +19,7 @@ import reactor.core.publisher.Mono; import java.util.Collection; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,9 +28,14 @@ import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.codec.Transcoder; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.transactions.components.TransactionLinks; public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { @@ -49,7 +55,7 @@ public ReactiveRemoveById removeById() { @Override public ReactiveRemoveById removeById(Class domainType) { return new ReactiveRemoveByIdSupport(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { @@ -63,10 +69,15 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; + private final AttemptContextReactive txCtx; + + private final TransactionLinks tl = new TransactionLinks(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), false, Optional.empty(), Optional.empty()); ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas) { + final DurabilityLevel durabilityLevel, Long cas, AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -76,23 +87,31 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.cas = cas; + this.txCtx = txCtx; } @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("removeById {}", pArgs); - return Mono.just(id) - .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())) - .map(r -> RemoveResult.from(docId, r))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive(); + Mono removeResult; + if (pArgs.getCtx() == null) { + removeResult = rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); + } else { + Transcoder transcoder = template.getCouchbaseClientFactory().getCluster().environment().transcoder(); + TransactionGetResult doc = new TransactionGetResult(id, null, 0, rc, tl, null, Optional.empty(), transcoder, + null); + removeResult = pArgs.getCtx().remove(doc).map(r -> new RemoveResult(id, 0, null)); + } + return removeResult.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); } @Override @@ -108,7 +127,7 @@ private RemoveOptions buildRemoveOptions(RemoveOptions options) { public RemoveByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override @@ -116,33 +135,40 @@ public RemoveByIdInScope withDurability(final PersistTo persistTo, final Replica Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override - public RemoveByIdWithDurability inCollection(final String collection) { + public RemoveByIdTxOrNot inCollection(final String collection) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdInCollection inScope(final String scope) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public TerminatingRemoveById withOptions(final RemoveOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdWithDurability withCas(Long cas) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); + } + + @Override + public RemoveByIdWithCas transaction(AttemptContextReactive txCtx) { + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, cas, txCtx); } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 7619eabf1..b91678672 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -24,9 +24,11 @@ import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.transactions.AttemptContextReactive; /** * RemoveBy Query Operations @@ -54,31 +56,62 @@ interface TerminatingRemoveByQuery { } /** - * Fluent methods to specify the query + * Fluent method to specify options. * - * @param the entity type. + * @param the entity type to use for the results. */ - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { + interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + TerminatingRemoveByQuery withOptions(QueryOptions options); + } - TerminatingRemoveByQuery matching(Query query); + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { + + @Deprecated + RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { + @Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } } /** - * Fluent method to specify options. + * Fluent method to specify the transaction * * @param the entity type to use for the results. */ - interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { + interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { /** - * Fluent method to specify options to use for execution + * Provide the transaction * - * @param options to use for execution + * @param txCtx - transaction */ - RemoveByQueryWithQuery withOptions(QueryOptions options); + @Override + TerminatingRemoveByQuery transaction(AttemptContextReactive txCtx); + } + + interface RemoveByQueryTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} + + /** + * Fluent methods to specify the query + * + * @param the entity type. + */ + interface RemoveByQueryWithQuery extends RemoveByQueryTxOrNot, WithQuery { + + RemoveByQueryTxOrNot matching(Query query); + + default RemoveByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } /** @@ -86,13 +119,13 @@ interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQue * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithOptions inCollection(String collection); + RemoveByQueryWithQuery inCollection(String collection); } /** @@ -109,25 +142,11 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { - - @Deprecated - RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - @Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ReactiveRemoveByQuery extends RemoveByQueryWithConsistency {} + interface ReactiveRemoveByQuery extends RemoveByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index d6ea991d6..f48428eb0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -22,14 +22,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; +import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionQueryOptions; public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQueryOperation { @@ -44,7 +48,7 @@ public ReactiveRemoveByQueryOperationSupport(final ReactiveCouchbaseTemplate tem @Override public ReactiveRemoveByQuery removeByQuery(Class domainType) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null); + return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null, null); } static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery { @@ -56,9 +60,11 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final String scope; private final String collection; private final QueryOptions options; + private final AttemptContextReactive txCtx; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + AttemptContextReactive txCtx) { this.template = template; this.domainType = domainType; this.query = query; @@ -66,19 +72,27 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery this.scope = scope; this.collection = collection; this.options = options; + this.txCtx = txCtx; } @Override public Flux all() { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); String statement = assembleDeleteQuery(pArgs.getCollection()); LOG.trace("removeByQuery {} statement: {}", pArgs, statement); - Mono allResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())); - return Flux.defer(() -> allResult.onErrorMap(throwable -> { + Mono allResult = null; + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); + if (pArgs.getCtx() == null) { + QueryOptions opts = buildQueryOptions(pArgs.getOptions()); + allResult = pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(buildQueryOptions(pArgs.getOptions())); + allResult = pArgs.getScope() == null ? txCtx.query(statement, opts) : pArgs.getCtx().query(rs, statement, opts); + } + Mono finalAllResult = allResult; + return Flux.defer(() -> finalAllResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { @@ -94,29 +108,34 @@ private QueryOptions buildQueryOptions(QueryOptions options) { return query.buildQueryOptions(options, qsc); } + private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { + TransactionQueryOptions txOptions = TransactionQueryOptions.queryOptions(); + return txOptions; + } + @Override - public TerminatingRemoveByQuery matching(final Query query) { + public RemoveByQueryTxOrNot matching(final Query query) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override - public RemoveByQueryWithConsistency inCollection(final String collection) { + public RemoveByQueryWithQuery inCollection(final String collection) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override @Deprecated public RemoveByQueryInScope consistentWith(final QueryScanConsistency scanConsistency) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } private String assembleDeleteQuery(String collection) { @@ -127,13 +146,19 @@ private String assembleDeleteQuery(String collection) { public RemoveByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryInCollection inScope(final String scope) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); + } + + @Override + public RemoveByQueryWithConsistency transaction(final AttemptContextReactive txCtx) { + return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, + options, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index 0341096cb..25f0b95c5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -27,11 +27,13 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; /** * ReplaceOperations @@ -84,19 +86,39 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } + interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { + + ReplaceByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + ReplaceByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + + interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { + @Override + ReplaceByIdWithExpiry transaction(final AttemptContextReactive txCtx); + } + + interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdWithOptions inCollection(String collection); + ReplaceByIdTxOrNot inCollection(String collection); } /** @@ -114,24 +136,11 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope inScope(String scope); } - interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { - - ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); - - ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ReactiveReplaceById extends ReplaceByIdWithExpiry {} + interface ReactiveReplaceById extends ReplaceByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index ba96de24c..a363cf697 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -20,18 +20,25 @@ import java.time.Duration; import java.util.Collection; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.util.Assert; +import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.components.TransactionLinks; public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { @@ -46,7 +53,7 @@ public ReactiveReplaceByIdOperationSupport(final ReactiveCouchbaseTemplate templ public ReactiveReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveReplaceByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null, template.support()); + DurabilityLevel.NONE, null, null, template.support()); } static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { @@ -60,11 +67,17 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final AttemptContextReactive txCtx; private final ReactiveTemplateSupport support; + private final TransactionLinks tl = new TransactionLinks(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), false, Optional.empty(), Optional.empty()); + ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { + final DurabilityLevel durabilityLevel, final Duration expiry, final AttemptContextReactive txCtx, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -74,26 +87,55 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; this.support = support; } @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("replaceById {}", pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .replace(converted.getId(), converted.export(), - buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyUpdatedCas(object, converted, result.cas()))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive(); + Mono reactiveEntity; + if (pArgs.getCtx() == null) { + reactiveEntity = support.encodeEntity(object) + .flatMap(converted -> rc + .replace(converted.getId(), converted.export(), + buildReplaceOptions(pArgs.getOptions(), object, converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + + } else { + reactiveEntity = support.encodeEntity(object) + .flatMap(converted -> pArgs.getCtx() + .replace(getTransactionHolder(object).transactionGetResult(), + converted.getContent()/*buildTranasactionOptions(pArgs.getOptions(), object, converted)*/) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), + new TransactionResultHolder(result)))); + + } + return reactiveEntity.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + } + + private TransactionResultHolder getTransactionHolder(T object) { + TransactionResultHolder transactionResultHolder; + + transactionResultHolder = template.support().getTxResultHolder(object); + if (transactionResultHolder == null) { + throw new CouchbaseException( + "TransactionResult from entity is null - was the entity obtained in a transaction?"); + } + return transactionResultHolder; + } + + private Object buildTransactionOptions(ReplaceOptions options, T object, CouchbaseDocument doc) { + return OptionsBuilder.buildTransactionReplaceOptions(buildReplaceOptions(options, object, doc)); } @Override @@ -110,26 +152,26 @@ private ReplaceOptions buildReplaceOptions(ReplaceOptions options, T object, Cou public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override - public ReplaceByIdWithDurability inCollection(final String collection) { + public ReplaceByIdTxOrNot inCollection(final String collection) { return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public ReplaceByIdInCollection inScope(final String scope) { return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public ReplaceByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override @@ -137,14 +179,21 @@ public ReplaceByIdInScope withDurability(final PersistTo persistTo, final Rep Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public ReplaceByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); + } + + @Override + public ReplaceByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, txCtx, support); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 0fa725574..cd0a8e0e8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -19,6 +19,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; /** * @@ -28,15 +29,14 @@ public interface ReactiveTemplateSupport { Mono encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection); + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txHolder); - Mono applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); - - Mono applyUpdatedId(T entity, Object id); + Mono applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder); Long getCas(Object entity); String getJavaNameForEntity(Class clazz); - void maybeEmitEvent(CouchbaseMappingEvent event); + TransactionResultHolder getTxResultHolder(T source); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java index 05249e198..b3045a25b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java @@ -86,19 +86,32 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } + interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { + @Override + UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithOptions inCollection(String collection); + UpsertByIdWithExpiry inCollection(String collection); } /** @@ -116,25 +129,11 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope inScope(String scope); } - interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { - @Override - UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ReactiveUpsertById extends UpsertByIdWithExpiry {} + interface ReactiveUpsertById extends UpsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index e84fe1fca..ec782fa28 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -79,14 +79,13 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, null, domainType); LOG.trace("upsertById {}", pArgs); return Mono.just(object).flatMap(support::encodeEntity) .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive() .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -113,7 +112,7 @@ public TerminatingUpsertById withOptions(final UpsertOptions options) { } @Override - public UpsertByIdWithDurability inCollection(final String collection) { + public UpsertByIdWithExpiry inCollection(final String collection) { return new ReactiveUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 084b1b718..cad2dfccf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -17,6 +17,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; /** * @@ -26,15 +27,15 @@ public interface TemplateSupport { CouchbaseDocument encodeEntity(Object entityToEncode); - T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); - T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder); - T applyUpdatedId(T entity, Object id); - - long getCas(Object entity); + Long getCas(Object entity); String getJavaNameForEntity(Class clazz); void maybeEmitEvent(CouchbaseMappingEvent event); + + TransactionResultHolder getTxResultHolder(T source); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 8dff2e25f..e7481b91f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -47,6 +47,7 @@ import org.springframework.data.couchbase.core.mapping.id.IdPrefix; import org.springframework.data.couchbase.core.mapping.id.IdSuffix; import org.springframework.data.couchbase.core.query.N1qlJoin; +import org.springframework.data.couchbase.repository.TransactionResult; import org.springframework.data.mapping.Alias; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.AssociationHandler; @@ -268,6 +269,9 @@ protected R read(final CouchbasePersistentEntity entity, final CouchbaseD entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (prop.isAnnotationPresent(TransactionResult.class)) { + return; + } if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop) || isIdConstructionProperty(prop) || prop.isAnnotationPresent(N1qlJoin.class)) { return; @@ -510,6 +514,10 @@ protected void writeInternal(final Object source, final CouchbaseDocument target entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (prop.isAnnotationPresent(TransactionResult.class)) { + return; + } + if (prop.equals(idProperty) || (versionProperty != null && prop.equals(versionProperty))) { return; } else if (prop.isAnnotationPresent(N1qlJoin.class)) { diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index af9ed2941..00c92153b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -24,6 +24,7 @@ import org.springframework.context.EnvironmentAware; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.env.Environment; +import org.springframework.data.couchbase.repository.TransactionResult; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.util.TypeInformation; @@ -40,6 +41,7 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity, EnvironmentAware { private Environment environment; + private CouchbasePersistentProperty transactionResult; /** * Create a new entity. @@ -69,6 +71,9 @@ public void setEnvironment(Environment environment) { @Override protected CouchbasePersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull( CouchbasePersistentProperty property) { + + transactionResult = property.findAnnotation(TransactionResult.class) != null ? property : transactionResult; + if (!property.isIdProperty()) { return null; } @@ -164,4 +169,9 @@ public boolean isTouchOnRead() { return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0; } + @Override + public CouchbasePersistentProperty transactionResultProperty() { + return transactionResult; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java index f768aab9c..c73a19513 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java @@ -61,4 +61,11 @@ public interface CouchbasePersistentEntity extends PersistentEntity entry : optsJson.toMap().entrySet()) { + txOptions.raw(entry.getKey(), entry.getValue()); + } + + if (LOG.isTraceEnabled()) { + LOG.trace("query options: {}", optsJson); + } + return txOptions; + } + public static ExistsOptions buildExistsOptions(ExistsOptions options) { options = options != null ? options : ExistsOptions.existsOptions(); return options; @@ -114,6 +134,13 @@ public static InsertOptions buildInsertOptions(InsertOptions options, PersistTo return options; } + public static TransactionInsertOptions buildTxInsertOptions(InsertOptions options) { + options = options != null ? options : InsertOptions.insertOptions(); + InsertOptions.Built built = options.build(); + TransactionInsertOptions txOptions = TransactionInsertOptions.insertOptions(); + return txOptions; + } + public static UpsertOptions buildUpsertOptions(UpsertOptions options, PersistTo persistTo, ReplicateTo replicateTo, DurabilityLevel durabilityLevel, Duration expiry, CouchbaseDocument doc) { options = options != null ? options : UpsertOptions.upsertOptions(); @@ -155,6 +182,18 @@ public static ReplaceOptions buildReplaceOptions(ReplaceOptions options, Persist return options; } + public static Object buildTransactionReplaceOptions(ReplaceOptions options) { + ReplaceOptions.Built built = options.build(); + TransactionReplaceOptions txOptions = TransactionReplaceOptions.replaceOptions(); + return txOptions; + } + + public static TransactionReplaceOptions buildTransactionUpsertOptions(ReplaceOptions options) { + ReplaceOptions.Built built = options.build(); + TransactionReplaceOptions txOptions = TransactionReplaceOptions.replaceOptions(); + return txOptions; + } + public static RemoveOptions buildRemoveOptions(RemoveOptions options, PersistTo persistTo, ReplicateTo replicateTo, DurabilityLevel durabilityLevel, Long cas) { options = options != null ? options : RemoveOptions.removeOptions(); @@ -423,4 +462,5 @@ public static String annotationString(Class annotation AnnotatedElement[] elements) { return annotationString(annotation, "value", defaultValue, elements); } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index fb1bc9daa..0de015e5a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -19,19 +19,23 @@ import static org.springframework.data.couchbase.core.query.OptionsBuilder.getCollectionFrom; import static org.springframework.data.couchbase.core.query.OptionsBuilder.getScopeFrom; +import com.couchbase.client.core.error.CouchbaseException; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.transactions.AttemptContextReactive; public class PseudoArgs { private final OPTS options; private final String scopeName; private final String collectionName; + private final AttemptContextReactive ctx; - public PseudoArgs(String scopeName, String collectionName, OPTS options) { + public PseudoArgs(String scopeName, String collectionName, OPTS options, AttemptContextReactive ctx) { this.options = options; this.scopeName = scopeName; this.collectionName = collectionName; + this.ctx = ctx; } /** @@ -47,11 +51,12 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options) { * @param domainType - entity that may have annotations */ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, - Class domainType) { + AttemptContextReactive ctx, Class domainType) { String scopeForQuery = null; String collectionForQuery = null; OPTS optionsForQuery = null; + AttemptContextReactive ctxForQuery = null; // 1) repository from DynamicProxy via template threadLocal - has precedence over annotation @@ -61,16 +66,18 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle scopeForQuery = threadLocal.getScope(); collectionForQuery = threadLocal.getCollection(); optionsForQuery = threadLocal.getOptions(); + ctxForQuery = threadLocal.getCtx(); } scopeForQuery = fromFirst(null, scopeForQuery, scope, getScopeFrom(domainType)); collectionForQuery = fromFirst(null, collectionForQuery, collection, getCollectionFrom(domainType)); optionsForQuery = fromFirst(null, options, optionsForQuery); + ctxForQuery = fromFirst( null, ctx, ctxForQuery); // if a collection was specified but no scope, use the scope from the clientFactory if (collectionForQuery != null && scopeForQuery == null) { - scopeForQuery = template.getCouchbaseClientFactory().getScope().name(); + scopeForQuery = template.getScopeName(); } // specifying scope and collection = _default is not necessary and will fail if server doesn't have collections @@ -84,7 +91,11 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle this.scopeName = scopeForQuery; this.collectionName = collectionForQuery; + if( scopeForQuery != null && collectionForQuery == null){ + throw new CouchbaseException(new IllegalArgumentException("if scope is not default or null, then collection must be specified")); + } this.options = optionsForQuery; + this.ctx = ctxForQuery; } @@ -109,8 +120,16 @@ public String getCollection() { return this.collectionName; } + /** + * @return the attempt context + */ + public AttemptContextReactive getCtx() { + return ctx; + } + @Override public String toString() { return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions(); } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java b/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java new file mode 100644 index 000000000..1399235a3 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java @@ -0,0 +1,33 @@ +/* + * 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.couchbase.core.support; + +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * A common interface operations that take scan consistency + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithCas { + /** + * Specify scan consistency + * + * @param cas + */ + Object withCas(Long cas); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java new file mode 100644 index 000000000..77202774e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java @@ -0,0 +1,33 @@ +/* + * 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.couchbase.core.support; + +import com.couchbase.transactions.AttemptContextReactive; + +/** + * Interface for operations that take distinct fields + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithTransaction { + /** + * Specify transactions + * + * @param txCtx + */ + Object transaction(AttemptContextReactive txCtx); +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java index 01e56fb32..883864c4d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -22,6 +22,7 @@ import org.springframework.data.couchbase.repository.support.DynamicInvocationHandler; import com.couchbase.client.java.CommonOptions; +import com.couchbase.transactions.AttemptContextReactive; /** * The generic parameter needs to be REPO which is either a CouchbaseRepository parameterized on T,ID or a @@ -51,7 +52,7 @@ public interface DynamicProxyable { @SuppressWarnings("unchecked") default REPO withOptions(CommonOptions options) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), - this.getClass().getInterfaces(), new DynamicInvocationHandler(this, options, null, (String) null)); + this.getClass().getInterfaces(), new DynamicInvocationHandler(this, options, null, null, null)); return proxyInstance; } @@ -61,7 +62,7 @@ default REPO withOptions(CommonOptions options) { @SuppressWarnings("unchecked") default REPO withScope(String scope) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), - this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, scope)); + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, scope, null)); return proxyInstance; } @@ -71,7 +72,17 @@ default REPO withScope(String scope) { @SuppressWarnings("unchecked") default REPO withCollection(String collection) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), - this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, collection, null)); + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, collection, null, null)); + return proxyInstance; + } + + /** + * @param ctx - the AttemptContextReactive for transactions + */ + @SuppressWarnings("unchecked") + default REPO withTransaction(AttemptContextReactive ctx) { + REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, null, ctx)); return proxyInstance; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java b/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java new file mode 100644 index 000000000..6e9be67da --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 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.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * Indicates the field should hold Transaction*Result and should NOT be considered part of the document. + * + * @author Michael Reiche + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +@Documented +@QueryAnnotation +public @interface TransactionResult { + + String value() default ""; + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index dedba009d..647f1a562 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -29,6 +29,7 @@ import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import com.couchbase.client.java.CommonOptions; +import com.couchbase.transactions.AttemptContextReactive; /** * Invocation Handler for scope/collection/options proxy for repositories @@ -44,9 +45,11 @@ public class DynamicInvocationHandler implements InvocationHandler { final ReactiveCouchbaseTemplate reactiveTemplate; CommonOptions options; String collection; - String scope;; + String scope; + AttemptContextReactive ctx; - public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope) { + public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope, + AttemptContextReactive ctx) { this.target = target; if (target instanceof CouchbaseRepository) { reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); @@ -60,6 +63,7 @@ public DynamicInvocationHandler(T target, CommonOptions options, String colle this.options = options; this.collection = collection; this.scope = scope; + this.ctx = ctx; this.repositoryClass = target.getClass(); } @@ -75,17 +79,22 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (method.getName().equals("withOptions")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, (CommonOptions) args[0], collection, scope)); + new DynamicInvocationHandler<>(target, (CommonOptions) args[0], collection, scope, ctx)); } if (method.getName().equals("withScope")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, options, collection, (String) args[0])); + new DynamicInvocationHandler<>(target, options, collection, (String) args[0], ctx)); } if (method.getName().equals("withCollection")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, options, (String) args[0], scope)); + new DynamicInvocationHandler<>(target, options, (String) args[0], scope, ctx)); + } + + if (method.getName().equals("withTransaction")) { + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), + new DynamicInvocationHandler<>(target, options, collection, scope, (AttemptContextReactive) args[0])); } Class[] paramTypes = null; @@ -117,7 +126,7 @@ private void setThreadLocal() { if (reactiveTemplate.getPseudoArgs() != null) { throw new RuntimeException("pseudoArgs not yet consumed by previous caller"); } - reactiveTemplate.setPseudoArgs(new PseudoArgs(this.scope, this.collection, this.options)); + reactiveTemplate.setPseudoArgs(new PseudoArgs(this.scope, this.collection, this.options, this.ctx)); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java index 8c5a48226..7ae667acf 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java @@ -151,14 +151,14 @@ public void deleteAll(Iterable entities) { @Override public long count() { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).count(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).count(); } @Override public void deleteAll() { - operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).all(); + operations.removeByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).all(); } @Override @@ -189,8 +189,8 @@ public Page findAll(Pageable pageable) { * @return the list of found entities, already executed. */ private List findAll(Query query) { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).matching(query).all(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()).matching(query) + .withConsistency(buildQueryScanConsistency()).all(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 4d5a3c0e6..dc99e37a4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -196,19 +196,19 @@ public Mono deleteAll(Publisher entityStream) { @Override public Mono count() { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).count(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).count(); } @Override public Mono deleteAll() { - return operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).all().then(); + return operations.removeByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).all().then(); } private Flux findAll(Query query) { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).matching(query).all(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()).matching(query) + .withConsistency(buildQueryScanConsistency()).all(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java new file mode 100644 index 000000000..c3282a489 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 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.repository.support; + +import com.couchbase.transactions.SingleQueryTransactionResult; +import com.couchbase.transactions.TransactionGetResult; + +/** + * Holds previously obtained Transaction*Result + * + * @author Michael Reiche + */ +public class TransactionResultHolder { + + TransactionGetResult getResult; + SingleQueryTransactionResult singleQueryResult; + + public TransactionResultHolder(TransactionGetResult getResult) { + // we don't need the content and we don't have access to the transcoder an txnMeta (and we don't need them either). + this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), + getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); + this.singleQueryResult = null; + } + + public TransactionResultHolder(SingleQueryTransactionResult singleQueryResult) { + this.getResult = null; + this.singleQueryResult = singleQueryResult; + } + + public TransactionGetResult transactionGetResult() { + return getResult; + } + + public SingleQueryTransactionResult singleQueryResult() { + return singleQueryResult(); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index dcd13b5ab..45e3378bb 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -68,7 +69,6 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryScanConsistency; /** * Query tests Theses tests rely on a cb server running This class tests collection support with @@ -104,18 +104,12 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); } @AfterEach @@ -124,8 +118,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); // then call the super method super.afterEach(); } @@ -138,8 +131,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -181,8 +174,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -206,8 +199,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(daveUsers).all(); + .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -223,17 +216,17 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all().collectList().block(); + .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); - couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(REQUEST_PLUS).all(); } @@ -248,8 +241,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -273,8 +266,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) + .withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -297,18 +290,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -316,8 +309,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count2); } finally { @@ -341,26 +334,29 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count() - .block(); + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); - // count (distinct { iata, icao } ) - Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block(); + // count( distinct (all fields in icaoClass) // which only has one field + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Class icaoClass = (new Object() { + String icao; + }).getClass(); + long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(icaoClass).withConsistency(REQUEST_PLUS).count().block(); assertEquals(7, count2); } finally { @@ -433,9 +429,8 @@ public void findByQuery() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("441")); try { - List found = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()); @@ -486,9 +481,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("495")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName) + .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -577,9 +572,8 @@ public void findByQueryOther() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("594")); try { - List found = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()); @@ -630,9 +624,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("648")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -695,9 +689,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options).all()); + assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all()); } @Test @@ -735,9 +728,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all()); + () -> couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) + .withOptions(options).all()); } @Test @@ -761,9 +754,8 @@ public void testScopeCollectionAnnotation() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) + .matching(query).withConsistency(REQUEST_PLUS).all(); assertEquals(saved, found.get(0), "should have found what was saved"); List notfound = couchbaseTemplate.findByQuery(UserCol.class).inScope(CollectionIdentifier.DEFAULT_SCOPE) .inCollection(CollectionIdentifier.DEFAULT_COLLECTION).matching(query).all(); @@ -785,9 +777,8 @@ public void testScopeCollectionRepoWith() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) + .matching(query).withConsistency(REQUEST_PLUS).all(); assertEquals(saved, found.get(0), "should have found what was saved"); List notfound = couchbaseTemplate.findByQuery(UserCol.class).inScope(CollectionIdentifier.DEFAULT_SCOPE) .inCollection(CollectionIdentifier.DEFAULT_COLLECTION).matching(query).all(); @@ -808,29 +799,28 @@ void testFluentApi() { DurabilityLevel dl = DurabilityLevel.NONE; User result; RemoveResult rr; - result = couchbaseTemplate.insertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + result = couchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); assertEquals(user1,result); - result = couchbaseTemplate.upsertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + result = couchbaseTemplate.upsertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); assertEquals(user1,result); - result = couchbaseTemplate.replaceById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + result = couchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); assertEquals(user1,result); - rr = couchbaseTemplate.removeById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + rr = couchbaseTemplate.removeById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1.getId()); assertEquals(rr.getId(), user1.getId()); assertEquals(user1,result); - result = reactiveCouchbaseTemplate.insertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + result = reactiveCouchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1).block(); assertEquals(user1,result); - result = reactiveCouchbaseTemplate.upsertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + result = reactiveCouchbaseTemplate.upsertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1).block(); assertEquals(user1,result); - result = reactiveCouchbaseTemplate.replaceById(User.class).withDurability(dl).inScope(scopeName) - .inCollection(collectionName).one(user1).block(); + result = reactiveCouchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl).one(user1).block(); assertEquals(user1,result); - rr = reactiveCouchbaseTemplate.removeById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + rr = reactiveCouchbaseTemplate.removeById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1.getId()).block(); assertEquals(rr.getId(), user1.getId()); } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index e6e749f12..3b8f6060a 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -129,8 +129,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where(i("firstname")).like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).matching(specialUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -144,7 +144,7 @@ void findAssessmentDO() { Query specialUsers = new Query(QueryCriteria.where(i("id")).is(ado.getId())); final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class) - .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals("123", foundUsers.get(0).getId(), "id"); assertEquals("44444444", foundUsers.get(0).getDocumentId(), "documentId"); assertEquals(ado, foundUsers.get(0)); @@ -172,7 +172,7 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).matching(daveUsers).all(); + .as(UserSubmissionProjected.class).matching(daveUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -189,11 +189,11 @@ void findByMatchingQueryProjected() { Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(REQUEST_PLUS).matching(specialUsers).all().collectList().block(); + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(1, foundUsersReactive.size()); couchbaseTemplate.removeById(User.class).all(Arrays.asList(user1.getId(), user2.getId(), specialUser.getId())); @@ -230,7 +230,7 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where(i("firstname")).notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).matching(nonSpecialUsers).withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).one(user2.getId())); @@ -267,6 +267,15 @@ void distinct() { .as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); + // count( distinct (all fields in icaoClass) + Class icaoClass = (new Object() { + String iata; + String icao; + }).getClass(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(REQUEST_PLUS).count(); + assertEquals(7, count2); + } finally { couchbaseTemplate.removeById() .all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); @@ -298,7 +307,8 @@ void distinctReactive() { assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) .as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); @@ -328,8 +338,8 @@ void sortedTemplate() { .query(QueryCriteria.where("iata").isNotNull()); Pageable pageableWithSort = PageRequest.of(0, 7, Sort.by("iata")); query.with(pageableWithSort); - List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) - .matching(query).all(); + List airports = couchbaseTemplate.findByQuery(Airport.class).matching(query).withConsistency(REQUEST_PLUS) + .all(); String[] sortedIatas = iatas.clone(); System.out.println("" + iatas.length + " " + sortedIatas.length); diff --git a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java index 42edc672a..62b81b1d3 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core.query; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -64,7 +65,6 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryScanConsistency; /** * Query tests Theses tests rely on a cb server running This class tests collection support with @@ -101,18 +101,12 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); } @AfterEach @@ -121,8 +115,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); // then call the super method super.afterEach(); } @@ -135,8 +128,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -178,8 +171,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -203,8 +196,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(daveUsers).all(); + .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -220,13 +213,13 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all().collectList().block(); + .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); } @@ -242,8 +235,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -267,8 +260,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) + .withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -291,18 +284,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -310,8 +303,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count2); } finally { @@ -335,26 +328,25 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count() - .block(); + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); // count( distinct { iata, icao } ) - Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block(); + Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] { "iata", "icao" }) + .withConsistency(REQUEST_PLUS).count().block(); assertEquals(7, count2); } finally { @@ -427,8 +419,8 @@ public void findByQuery() { // 4 Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowa")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(scopeName).inCollection(collectionName).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()).block(); @@ -479,10 +471,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowe")).block(); - List removeResults = template.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList() - .block(); + List removeResults = template.removeByQuery(Airport.class).inScope(scopeName) + .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -526,7 +517,6 @@ public void existsByIdOther() { // 1 ExistsOptions existsOptions = ExistsOptions.existsOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowg")).block(); - try { Boolean exists = template.existsById().inScope(otherScope).inCollection(otherCollection) .withOptions(existsOptions).one(vie.getId()).block(); @@ -571,8 +561,8 @@ public void findByQueryOther() { // 4 Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowj")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); @@ -623,10 +613,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lown")).block(); - List removeResults = template.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList() - .block(); + List removeResults = template.removeByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -689,9 +678,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, - () -> template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).all().collectList().block()); + assertThrows(AmbiguousTimeoutException.class, () -> template.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block()); } @Test @@ -729,9 +717,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> template.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block()); + () -> template.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) + .withOptions(options).all().collectList().block()); } @Test diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index bb436d6b5..885d180ce 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -47,6 +47,7 @@ public class Airport extends ComparableEntity { @Expiration private long expiration; @Max(2) long size; + private long someNumber; @PersistenceConstructor public Airport(String key, String iata, String icao) { diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index 9a66f5efa..204382859 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -66,6 +66,7 @@ public interface AirportRepository extends CouchbaseRepository, List findByIataInAndIcaoIn(java.util.Collection size, java.util.Collection color, Pageable pageable); + // override an annotate with REQUEST_PLUS @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List findAll(); @@ -92,6 +93,12 @@ List findByIataInAndIcaoIn(java.util.Collection size, java.util @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Airport findByIataIn(JsonArray iatas); + @Query("Select \"\" AS __id, 0 AS __cas, substr(iata,0,1) as iata, count(*) as someNumber FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} GROUP BY substr(iata,0,1)") + List groupByIata(); + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Airport findArchivedByIata(Iata iata); + // NOT_BOUNDED to test ScanConsistency // @ScanConsistency(query = QueryScanConsistency.NOT_BOUNDED) Airport iata(String iata); diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 9e72e582f..84d7e6ba9 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -231,7 +231,7 @@ public String typeKey() { return "t"; // this will override '_class', is passed in to new CustomMappingCouchbaseConverter } - static String scopeName = null; + public static String scopeName = null; @Override protected String getScopeName() { diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index eb2a4dd75..e90a37927 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -25,6 +25,8 @@ import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.core.mapping.Field; +import org.springframework.data.couchbase.repository.TransactionResult; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.lang.Nullable; @Document @@ -47,6 +49,11 @@ public class Person extends AbstractEntity { private Address address; + // Required for use in transactions (??) + //@Transient + //@TransactionResult private TransactionResultHolder txResultHolder; + + public Person() {} public Person(String firstname, String lastname) { @@ -61,19 +68,24 @@ public Person(int id, String firstname, String lastname) { setId(new UUID(id, id)); } + public Person(UUID id, String firstname, String lastname) { + this(firstname, lastname); + setId(id); + } + static String optional(String name, Optional obj) { if (obj != null) { if (obj.isPresent()) { - return (" " + name + ": '" + obj.get() + "'\n"); + return (" " + name + ": '" + obj.get() + "'"); } else { - return " " + name + ": null\n"; + return " " + name + ": null"; } } return ""; } - public Optional getFirstname() { - return firstname; + public String getFirstname() { + return firstname.get(); } public void setFirstname(String firstname) { @@ -84,8 +96,8 @@ public void setFirstname(Optional firstname) { this.firstname = firstname; } - public Optional getLastname() { - return lastname; + public String getLastname() { + return lastname.get(); } public void setLastname(String lastname) { @@ -131,7 +143,7 @@ public String toString() { sb.append(optional(", firstname", firstname)); sb.append(optional(", lastname", lastname)); if (middlename != null) - sb.append(", middlename : " + middlename); + sb.append(", middlename : '" + middlename + "'"); sb.append(", version : " + version); if (creator != null) { sb.append(", creator : " + creator); @@ -148,8 +160,27 @@ public String toString() { if (getAddress() != null) { sb.append(", address : " + getAddress().toString()); } - sb.append("}"); + sb.append("\n}"); return sb.toString(); } + public Person withFirstName(String firstName) { + Person p = new Person(this.getId(), firstName, this.getLastname()); + //p.txResultHolder = this.txResultHolder; + return p; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + + Person that = (Person) obj; + return this.getId().equals(that.getId()) && this.getFirstname().equals(that.getFirstname()) + && this.getLastname().equals(that.getLastname()) && this.getMiddlename().equals(that.getMiddlename()); + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java index 2c9985d74..c1579f3ef 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.UUID; +import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.repository.CrudRepository; @@ -28,7 +29,7 @@ /** * @author Michael Reiche */ -public interface PersonRepository extends CrudRepository { +public interface PersonRepository extends CrudRepository, DynamicProxyable { /* * These methods are exercised in HomeController of the test spring-boot DemoApplication diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java new file mode 100644 index 000000000..e28666513 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 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.domain; + +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; + +/** + * @author Michael Reiche + */ +public interface ReactivePersonRepository + extends ReactiveCouchbaseRepository, DynamicProxyable { + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index b24ff3aae..c085451de 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -18,6 +18,8 @@ import static com.couchbase.client.java.query.QueryScanConsistency.NOT_BOUNDED; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static com.couchbase.client.java.query.QueryOptions.queryOptions; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -107,6 +109,8 @@ import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryScanConsistency; /** @@ -277,8 +281,7 @@ void findBySimpleProperty() { try { vie = new Airport("airports::vie", "vie", "low6"); vie = airportRepository.save(vie); - Airport airport2 = airportRepository - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + Airport airport2 = airportRepository.withOptions(queryOptions().scanConsistency(REQUEST_PLUS)) .findByIata(vie.getIata()); assertEquals(airport2, vie); @@ -390,9 +393,10 @@ void findByTypeAlias() { vie = new Airport("airports::vie", "vie", "loww"); vie = airportRepository.save(vie); List airports = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS) .matching(org.springframework.data.couchbase.core.query.Query .query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))) + .withConsistency(REQUEST_PLUS) + .all(); assertFalse(airports.isEmpty(), "should have found aiport"); } finally { @@ -452,15 +456,13 @@ void findBySimplePropertyWithCollection() { Airport saved = airportRepository.withScope(scopeName).withCollection(collectionName).save(vie); // given collection (on scope used by template) Airport airport2 = airportRepository.withCollection(collectionName) - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) - .iata(vie.getIata()); + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS)).iata(vie.getIata()); assertEquals(saved, airport2); // given scope and collection Airport airport3 = airportRepository.withScope(scopeName).withCollection(collectionName) - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) - .iata(vie.getIata()); + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS)).iata(vie.getIata()); assertEquals(saved, airport3); // given bad collection @@ -468,7 +470,8 @@ void findBySimplePropertyWithCollection() { () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata())); // given bad scope - assertThrows(IndexFailureException.class, () -> airportRepository.withScope("bogusScope").iata(vie.getIata())); + assertThrows(IndexFailureException.class, + () -> airportRepository.withScope("bogusScope").withCollection(collectionName).iata(vie.getIata())); } finally { airportRepository.delete(vie); @@ -502,12 +505,11 @@ void findBySimplePropertyWithOptions() { try { Airport saved = airportRepository.save(vie); // Duration of 1 nano-second will cause timeout - assertThrows(AmbiguousTimeoutException.class, () -> airportRepository - .withOptions(QueryOptions.queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); + assertThrows(AmbiguousTimeoutException.class, + () -> airportRepository.withOptions(queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); - Airport airport3 = airportRepository.withOptions( - QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) - .iata(vie.getIata()); + Airport airport3 = airportRepository + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS).parameters(positionalParams)).iata(vie.getIata()); assertEquals(saved, airport3); } finally { @@ -525,7 +527,8 @@ public void saveNotBounded() { // set version == 0 so save() will be an upsert, not a replace Airport saved = airportRepository.save(vie.clearVersion()); try { - airport2 = airportRepository.iata(saved.getIata()); + airport2 = airportRepository.withOptions(queryOptions().scanConsistency(QueryScanConsistency.NOT_BOUNDED)) + .iata(saved.getIata()); if (airport2 == null) { break; } @@ -538,7 +541,8 @@ public void saveNotBounded() { assertEquals(vie.getId(), removeResult.getId()); assertTrue(removeResult.getCas() != 0); assertTrue(removeResult.getMutationToken().isPresent()); - Airport airport3 = airportRepository.iata(vie.getIata()); + Airport airport3 = airportRepository.withOptions(queryOptions().scanConsistency(REQUEST_PLUS) + .consistentWith(MutationState.from(removeResult.getMutationToken().get()))).iata(vie.getIata()); assertNull(airport3, "should have been removed"); } } @@ -705,6 +709,24 @@ void countSlicePage() { } } + @Test + void testGroupBy() { + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + try { + airportRepository.saveAll( + Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) + .collect(Collectors.toSet())); + List airports = airportRepository.groupByIata(); + for (Airport a : airports) { + System.out.println(a); + } + + } finally { + airportRepository + .deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); + } + } + @Test void badCount() { assertThrows(CouchbaseQueryExecutionException.class, () -> airportRepository.countBad()); @@ -860,7 +882,7 @@ void threadSafeStringParametersTest() throws Exception { void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDZ"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { airportRepository.saveAll(asList(vienna, frankfurt, losAngeles)); @@ -875,8 +897,8 @@ void deleteAllById() { void couchbaseRepositoryQuery() throws Exception { User user = new User("1", "Dave", "Wilson"); userRepository.save(user); - couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) - .matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")).all(); + couchbaseTemplate.findByQuery(User.class).matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")) + .withConsistency(REQUEST_PLUS).all(); String input = "findByFirstname"; Method method = UserRepository.class.getMethod(input, String.class); CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index 725a93b6a..cc3bd5f0d 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -215,7 +215,7 @@ void count() { void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDX"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { @@ -235,7 +235,7 @@ void deleteAllById() { void deleteAll() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDY"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index 748401ce3..b720ae59c 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -32,6 +32,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.AddressAnnotated; import org.springframework.data.couchbase.domain.Airport; @@ -222,9 +223,14 @@ public void testScopeCollectionAnnotationSwap() { // collection from CrudMethodMetadata of UserCol.save() UserCol userCol = new UserCol("1", "Dave", "Wilson"); Airport airport = new Airport("3", "myIata", "myIcao"); - UserCol savedCol = userColRepository.save(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata - userColRepository.delete(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata - assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); + try { + UserCol savedCol = userColRepository.save(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata + userColRepository.delete(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata + assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); + } finally { + couchbaseTemplate.removeByQuery(Airport.class).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } } // template default scope is my_scope @@ -278,8 +284,8 @@ void findPlusN1qlJoinBothAnnotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(scopeName).all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionAnnotatedRepository.findByUsername(user.getUsername()); @@ -333,8 +339,8 @@ void findPlusN1qlJoinUnannotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(scopeName).all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionUnannotatedRepository.findByUsername(user.getUsername()); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index 72f657d2c..50c663666 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -23,7 +23,6 @@ import java.util.Properties; import java.util.UUID; -import com.couchbase.client.java.query.QueryScanConsistency; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; @@ -58,6 +57,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * @author Michael Nitschinger * @author Michael Reiche @@ -99,8 +100,8 @@ queryMethod, converter, config().bucketname(), new SpelExpressionParser(), Query query = creator.createQuery(); - ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate - .findByQuery(Airline.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query); + ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate.findByQuery(Airline.class).matching(query) + .withConsistency(QueryScanConsistency.REQUEST_PLUS); Optional al = q.one(); assertEquals(airline.toString(), al.get().toString()); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java new file mode 100644 index 000000000..134a9ce9a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java @@ -0,0 +1,290 @@ +/* + * Copyright 2012-2021 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TRANSACTIONS; +import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; + +import org.junit.jupiter.api.Disabled; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.time.Duration; +import java.util.LinkedList; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +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.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.error.TransactionFailed; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +@Disabled +public class CouchbaseTransactionNativeTests extends CollectionAwareIntegrationTests { + + // @Autowired not supported on static fields. These are initialized in beforeAll() + // Also - @Autowired doesn't work here on couchbaseClientFactory even when it is not static, not sure why - oh, it + // seems there is not a ReactiveCouchbaseClientFactory bean + static CouchbaseClientFactory couchbaseClientFactory; + static ReactiveCouchbaseOperations operations; + static GenericApplicationContext appContext; + static Transactions transactions; + @Autowired ReactivePersonRepository rxPersonRepo; + + @BeforeAll + public static void beforeAll() { + + // AnnotationConfigApplicationContext() is going to create a Transactions object. + appContext = new AnnotationConfigApplicationContext(Config.class); + operations = appContext.getBean(ReactiveCouchbaseOperations.class); + couchbaseTemplate = (CouchbaseTemplate) appContext.getBean(COUCHBASE_TEMPLATE); + transactions = (Transactions) appContext.getBean(COUCHBASE_TRANSACTIONS); + reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) appContext.getBean(REACTIVE_COUCHBASE_TEMPLATE); + couchbaseClientFactory = (CouchbaseClientFactory) appContext.getBean(BeanNames.COUCHBASE_CLIENT_FACTORY); + + // this will initialize couchbaseTemplate and reactiveCouchbaseTemplate if not already initialized + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + try { + couchbaseClientFactory.close(); + } catch (IOException e) { + e.printStackTrace(); + } + appContext.close(); + callSuperAfterAll(new Object() {}); + } + + @Test + public void replacePersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, collectionName, person.getId().toString()); + couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return reactiveCouchbaseTemplate.findById(Person.class).inScope(null).inCollection(collectionName) + .transaction(ctx).one(person.getId().toString()).flatMap(pGet -> reactiveCouchbaseTemplate + .replaceById(Person.class).inCollection(collectionName).transaction(ctx).one(pGet.withFirstName("Walt"))) + .then(); + })); + result.block(); + Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) + .one(person.getId().toString()); + assertEquals(pFound, person.withFirstName("Walt"), "Should have found Walt"); + } + + @Test + public void insertReplacePersonsCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, collectionName, person.getId().toString()); + + Mono result = transactions.reactive((ctx) -> { // get the ctx + return reactiveCouchbaseTemplate.insertById(Person.class).inCollection(collectionName).transaction(ctx) + .one(person).flatMap(pInsert -> reactiveCouchbaseTemplate.replaceById(Person.class) + .inCollection(collectionName).transaction(ctx).one(pInsert.withFirstName("Walt"))) + .then(); + }); + + TransactionResult tr = result.block(); + Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) + .one(person.getId().toString()); + assertEquals(pFound, person.withFirstName("Walt"), "Should have found Walt"); + } + + @Test + public void deletePersonCBTransactionsRepo() { + Person person = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, collectionName, person.getId().toString()); + rxPersonRepo.withCollection(collectionName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxPersonRepo.withCollection(collectionName).withTransaction(ctx).deleteById(person.getId().toString()) + .then(rxPersonRepo.withCollection(collectionName).withTransaction(ctx).deleteById(person.getId().toString())) + .then(); + })); + assertThrows(TransactionFailed.class, () -> result.block()); + Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) + .one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void findPersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, collectionName, person.getId().toString()); + couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); + List docs = new LinkedList(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return reactiveCouchbaseTemplate.findByQuery(Person.class).inCollection(collectionName).matching(q) + .transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + })); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void insertPersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, collectionName, person.getId().toString()); + + Mono result = transactions.reactive((ctx) -> { // get the ctx + return reactiveCouchbaseTemplate.insertById(Person.class).inCollection(collectionName).transaction(ctx) + .one(person). flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailed e) { + e.printStackTrace(); + if (e.getCause() instanceof PoofException) { + Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) + .one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void replacePersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, collectionName, person.getId().toString()); + couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return reactiveCouchbaseTemplate.findById(Person.class).inCollection(collectionName).transaction(ctx) + .one(person.getId().toString()) + .flatMap(pFound -> reactiveCouchbaseTemplate.replaceById(Person.class).inCollection(collectionName) + .transaction(ctx).one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) + .one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void findPersonSpringTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, collectionName, person.getId().toString()); + couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); + List docs = new LinkedList(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return reactiveCouchbaseTemplate.findByQuery(Person.class).inCollection(collectionName).matching(q) + .transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + }); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + void remove(Mono col, String id) { + remove(col.block(), id); + } + + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + try { + couchbaseTemplate.removeById(Person.class).inCollection(collection).one(id); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + static class PoofException extends Exception {}; +} diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 6e96c5e7a..1a033b542 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -178,16 +178,13 @@ private static void callSuper(Object createdHere, Class annotationClass) { for (Method m : methods) { annotation = m.getAnnotation(annotationClass); if (annotation != null) { - if (annotation != null) { - m.invoke(null); - invokedSuper = m; - } + m.invoke(null); + invokedSuper = m; } } if (invokedSuper != null) { // called method is responsible for calling any super methods return; } - } } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index 90b9ab0c8..c535755b1 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -111,9 +111,12 @@ public static void beforeAll() { } catch (IOException ioe) { throw new RuntimeException(ioe); } - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + // This will result in a Transactions object being created. + if (couchbaseTemplate == null || reactiveCouchbaseTemplate == null) { + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + } } /** diff --git a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java index 776283df9..36cbbe8df 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java @@ -83,7 +83,7 @@ private static Properties loadProperties() { defaults.load(url.openStream()); } } catch (Exception ex) { - throw new RuntimeException("Could not load properties", ex); + throw new RuntimeException("Could not load properties - maybe is pom instead of jar?", ex); } Properties all = new Properties(System.getProperties()); diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 9d05fdc15..6efee2b37 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -27,5 +27,7 @@ + " + From b4a93d7da0310b1c96ea0c25b891e6d2efa0ff49 Mon Sep 17 00:00:00 2001 From: mikereiche Date: Mon, 15 Nov 2021 18:32:36 -0800 Subject: [PATCH 2/9] Support for @Transactional for blocking and reactive Transactionmanager. Closes 1145. --- pom.xml | 71 +- .../AttemptContextReactiveAccessor.java | 90 ++ .../transactions/TransactionsReactive.java | 753 ++++++++++++ .../demo/CouchbaseTransactionManager.java | 209 ++++ .../demo/CouchbaseTransactionalTemplate.java | 78 ++ .../demo/SpringTransactionGetResult.java | 30 + .../couchbase/CouchbaseClientFactory.java | 12 + .../ReactiveCouchbaseClientFactory.java | 119 ++ .../SimpleCouchbaseClientFactory.java | 48 + .../SimpleReactiveCouchbaseClientFactory.java | 361 ++++++ .../AbstractCouchbaseConfiguration.java | 68 +- .../data/couchbase/config/BeanNames.java | 5 + .../core/AbstractTemplateSupport.java | 46 +- .../couchbase/core/CouchbaseOperations.java | 7 + .../couchbase/core/CouchbaseTemplate.java | 81 +- .../core/CouchbaseTemplateSupport.java | 29 +- .../core/ExecutableFindByIdOperation.java | 4 +- .../ExecutableFindByIdOperationSupport.java | 15 +- .../core/ExecutableFindByQueryOperation.java | 4 +- ...ExecutableFindByQueryOperationSupport.java | 8 +- .../core/ExecutableInsertByIdOperation.java | 4 +- .../ExecutableInsertByIdOperationSupport.java | 8 +- .../core/ExecutableRemoveByIdOperation.java | 4 +- .../ExecutableRemoveByIdOperationSupport.java | 10 +- .../ExecutableRemoveByQueryOperation.java | 4 +- ...ecutableRemoveByQueryOperationSupport.java | 11 +- .../core/ExecutableReplaceByIdOperation.java | 4 +- ...ExecutableReplaceByIdOperationSupport.java | 8 +- .../core/NonReactiveSupportWrapper.java | 31 +- .../core/ReactiveCouchbaseOperations.java | 17 +- .../core/ReactiveCouchbaseTemplate.java | 266 +++- .../ReactiveCouchbaseTemplateSupport.java | 33 +- .../ReactiveExistsByIdOperationSupport.java | 2 +- ...activeFindByAnalyticsOperationSupport.java | 4 +- .../core/ReactiveFindByIdOperation.java | 4 +- .../ReactiveFindByIdOperationSupport.java | 52 +- .../core/ReactiveFindByQueryOperation.java | 4 +- .../ReactiveFindByQueryOperationSupport.java | 68 +- ...eFindFromReplicasByIdOperationSupport.java | 4 +- .../core/ReactiveInsertByIdOperation.java | 4 +- .../ReactiveInsertByIdOperationSupport.java | 105 +- .../core/ReactiveRemoveByIdOperation.java | 4 +- .../ReactiveRemoveByIdOperationSupport.java | 18 +- .../core/ReactiveRemoveByQueryOperation.java | 8 +- ...ReactiveRemoveByQueryOperationSupport.java | 22 +- .../core/ReactiveReplaceByIdOperation.java | 6 +- .../ReactiveReplaceByIdOperationSupport.java | 90 +- .../core/ReactiveSessionCallback.java | 45 + .../core/ReactiveTemplateSupport.java | 20 +- .../ReactiveUpsertByIdOperationSupport.java | 29 +- .../data/couchbase/core/TemplateSupport.java | 10 +- .../convert/MappingCouchbaseConverter.java | 1 - .../couchbase/core/query/OptionsBuilder.java | 2 +- .../couchbase/core/support/PseudoArgs.java | 24 +- .../core/support/WithTransaction.java | 8 +- .../transaction/SDCouchbaseTransactions.java | 76 ++ .../repository/DynamicProxyable.java | 36 +- .../ReactiveCouchbaseRepository.java | 1 + .../couchbase/repository/TransactionMeta.java | 40 + .../support/CouchbaseRepositoryBase.java | 9 + .../support/DynamicInvocationHandler.java | 24 +- .../SimpleReactiveCouchbaseRepository.java | 25 + .../transaction/AbortCommitSubscriber.java | 67 + .../couchbase/transaction/ClientSession.java | 67 + .../transaction/ClientSessionImpl.java | 299 +++++ .../transaction/ClientSessionOptions.java | 115 ++ .../CouchbaseAttemptContextReactive.java | 103 ++ .../CouchbaseCallbackTransactionManager.java | 294 +++++ .../transaction/CouchbaseResourceHolder.java | 157 +++ .../transaction/CouchbaseStuffHandle.java | 144 +++ .../CouchbaseTransactionDefinition.java | 36 + .../CouchbaseTransactionManager.java | 544 +++++++++ .../CouchbaseTransactionStatus.java | 33 + ...hbaseTransactionalOperatorNonReactive.save | 197 +++ .../ReactiveCouchbaseClientUtils.java | 303 +++++ .../ReactiveCouchbaseResourceHolder.java | 155 +++ .../ReactiveCouchbaseTransactionManager.java | 540 ++++++++ .../couchbase/transaction/ServerSession.java | 19 + .../SessionAwareMethodInterceptor.java | 212 ++++ .../transaction/SessionSynchronization.java | 39 + .../transaction/TransactionOptions.java | 19 + .../transaction/TransactionsWrapper.java | 137 +++ .../internal/AsyncClientSession.java | 26 + .../internal/BaseClientSessionImpl.save | 170 +++ .../internal/ClientSessionPublisherImpl.save | 241 ++++ .../internal/SingleResultCallback.java | 10 + ...chbaseCacheCollectionIntegrationTests.java | 4 + .../cache/CouchbaseCacheIntegrationTests.java | 10 +- ...hbaseTemplateKeyValueIntegrationTests.java | 10 +- ...mplateQueryCollectionIntegrationTests.java | 56 +- ...ouchbaseTemplateQueryIntegrationTests.java | 8 + ...hbaseTemplateKeyValueIntegrationTests.java | 8 + ...mplateQueryCollectionIntegrationTests.java | 29 +- .../couchbase/domain/CollectionsConfig.java | 8 + .../data/couchbase/domain/Config.java | 20 +- .../data/couchbase/domain/FluxTest.java | 44 +- .../data/couchbase/domain/Person.java | 29 +- .../couchbase/domain/PersonRepository.java | 6 +- ...aseRepositoryKeyValueIntegrationTests.java | 12 + ...chbaseRepositoryQueryIntegrationTests.java | 2 +- ...sitoryQueryCollectionIntegrationTests.java | 30 +- ...sitoryQueryCollectionIntegrationTests.java | 7 + .../AfterTransactionAssertion.java | 33 + ...basePersonTransactionIntegrationTests.java | 1088 +++++++++++++++++ ...onTransactionReactiveIntegrationTests.java | 617 ++++++++++ ...uchbaseReactiveTransactionNativeTests.java | 439 +++++++ ...eTemplateTransaction2IntegrationTests.java | 211 ++++ ...seTemplateTransactionIntegrationTests.java | 195 +++ .../CouchbaseTransactionNativeTests.java | 290 ----- .../CouchbaseTransactionNativeTests.save | 368 ++++++ .../data/couchbase/transactions/ObjectId.java | 10 + .../SimulateFailureException.java | 15 + .../util/ClusterAwareIntegrationTests.java | 33 +- .../couchbase/util/JavaIntegrationTests.java | 49 +- .../data/couchbase/util/TestCluster.java | 2 +- .../couchbase/util/TestClusterConfig.java | 1 + 116 files changed, 10065 insertions(+), 679 deletions(-) create mode 100644 src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java create mode 100644 src/main/java/com/couchbase/transactions/TransactionsReactive.java create mode 100644 src/main/java/com/example/demo/CouchbaseTransactionManager.java create mode 100644 src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java create mode 100644 src/main/java/com/example/demo/SpringTransactionGetResult.java create mode 100644 src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java create mode 100644 src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/ReactiveSessionCallback.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/TransactionMeta.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ServerSession.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/SessionSynchronization.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java delete mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java diff --git a/pom.xml b/pom.xml index cfc94d39f..99a817e2b 100644 --- a/pom.xml +++ b/pom.xml @@ -21,8 +21,11 @@ 3.2.5 3.2.5 2.7.0-SNAPSHOT - spring.data.couchbase 1.2.2 + spring.data.couchbase + @@ -39,13 +42,32 @@ + + + com.couchbase.client couchbase-transactions ${couchbase-transactions} + + + com.couchbase.client + java-client + + - org.springframework spring-context-support @@ -67,12 +89,6 @@ ${springdata.commons} - - com.couchbase.client - java-client - ${couchbase} - - org.springframework spring-test @@ -189,7 +205,14 @@ com.squareup.okhttp3 okhttp - 4.4.0 + 4.8.1 + test + + + + com.squareup.okhttp3 + okhttp-tls + 4.8.1 test @@ -221,6 +244,36 @@ 4.0.3 test + + com.couchbase.client + couchbase-transactions + ${couchbase-transactions} + compile + + + com.couchbase.client + java-client + + + + + org.testcontainers + testcontainers + + + + com.couchbase.client + java-client + 3.2.5 + + + + ch.qos.logback + logback-classic + 1.2.5 + compile + + diff --git a/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java new file mode 100644 index 000000000..f59d110ed --- /dev/null +++ b/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java @@ -0,0 +1,90 @@ +/* +/* + * Copyright 2021 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 com.couchbase.transactions; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.transactions.config.MergedTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfigBuilder; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.forwards.Supported; +import com.couchbase.transactions.log.TransactionLogger; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; + +/** + * To access the AttemptContextReactive held by AttemptContext + * + * @author Michael Reiche + */ +public class AttemptContextReactiveAccessor { + + public static AttemptContextReactive getACR(AttemptContext attemptContext) { + return attemptContext.ctx(); + } + + public static AttemptContext from(AttemptContextReactive attemptContextReactive) { + return new AttemptContext(attemptContextReactive); + } + + public static TransactionLogger getLogger(AttemptContextReactive attemptContextReactive){ + return attemptContextReactive.LOGGER; + } + @Stability.Internal + public static AttemptContextReactive newAttemptContextReactive(TransactionsReactive transactions){ + PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); + MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); + + TransactionContext overall = new TransactionContext( + transactions.cleanup().clusterData().cluster().environment().requestTracer(), + transactions.cleanup().clusterData().cluster().environment().eventBus(), + UUID.randomUUID().toString(), now(), Duration.ZERO, merged); + + String txnId = UUID.randomUUID().toString(); + overall.LOGGER.info(configDebug(transactions.config(), perConfig)); + return transactions.createAttemptContext(overall, merged, txnId); + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + + static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { + StringBuilder sb = new StringBuilder(); + sb.append("library version: "); + sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); + sb.append(" config: "); + sb.append("atrs="); + sb.append(config.numAtrs()); + sb.append(", metadataCollection="); + sb.append(config.metadataCollection()); + sb.append(", expiry="); + sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); + sb.append("msecs durability="); + sb.append(config.durabilityLevel()); + sb.append(" per-txn config="); + sb.append(" durability="); + sb.append(perConfig.durabilityLevel()); + sb.append(", supported="); + sb.append(Supported.SUPPORTED); + return sb.toString(); + } + +} diff --git a/src/main/java/com/couchbase/transactions/TransactionsReactive.java b/src/main/java/com/couchbase/transactions/TransactionsReactive.java new file mode 100644 index 000000000..1e64e803e --- /dev/null +++ b/src/main/java/com/couchbase/transactions/TransactionsReactive.java @@ -0,0 +1,753 @@ +/* + * Copyright 2021 Couchbase, Inc. + * + * 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 + * + * http://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 com.couchbase.transactions; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.cnc.EventBus; +import com.couchbase.client.core.retry.reactor.DefaultRetry; +import com.couchbase.client.core.retry.reactor.Jitter; +import com.couchbase.client.core.retry.reactor.RetryContext; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.ReactiveScope; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.query.ReactiveQueryResult; +import com.couchbase.transactions.cleanup.ClusterData; +import com.couchbase.transactions.cleanup.TransactionsCleanup; +import com.couchbase.transactions.components.ATR; +import com.couchbase.transactions.components.ActiveTransactionRecord; +import com.couchbase.transactions.config.MergedTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfigBuilder; +import com.couchbase.transactions.config.SingleQueryTransactionConfig; +import com.couchbase.transactions.config.SingleQueryTransactionConfigBuilder; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.deferred.TransactionSerializedContext; +import com.couchbase.transactions.error.TransactionCommitAmbiguous; +import com.couchbase.transactions.error.TransactionExpired; +import com.couchbase.transactions.error.TransactionFailed; +import com.couchbase.transactions.error.internal.ErrorClasses; +import com.couchbase.transactions.error.external.TransactionOperationFailed; +import com.couchbase.transactions.forwards.Supported; +import com.couchbase.transactions.log.EventBusPersistedLogger; +import com.couchbase.transactions.log.PersistedLogWriter; +import com.couchbase.transactions.log.TransactionLogEvent; +import com.couchbase.transactions.support.AttemptContextFactory; +import com.couchbase.transactions.support.AttemptStates; +import com.couchbase.transactions.support.OptionsWrapperUtil; +import com.couchbase.transactions.util.DebugUtil; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import static com.couchbase.transactions.error.internal.TransactionOperationFailedBuilder.createError; +import static com.couchbase.transactions.log.PersistedLogWriter.MAX_LOG_ENTRIES_DEFAULT; +import static com.couchbase.transactions.support.SpanWrapperUtil.DB_COUCHBASE_TRANSACTIONS; + +/** + * An asynchronous version of {@link Transactions}, allowing transactions to be created and run in an asynchronous + * manner. + *

+ * The main method to run transactions is {@link TransactionsReactive#run}. + */ +public class TransactionsReactive { + static final int MAX_ATTEMPTS = 1000; + private final TransactionsCleanup cleanup; + private final TransactionConfig config; + private AttemptContextFactory attemptContextFactory; + private EventBusPersistedLogger persistedLogger; + + /** + * This is package-private. Applications should create a {@link Transactions} object instead, and then call {@link + * Transactions#reactive}. + */ + static TransactionsReactive create(Cluster cluster, TransactionConfig config) { + return new TransactionsReactive(cluster, config); + } + + private TransactionsReactive(Cluster cluster, TransactionConfig config) { + Objects.requireNonNull(cluster); + Objects.requireNonNull(config); + + ClusterData clusterData = new ClusterData(cluster); + this.config = config; + this.attemptContextFactory = config.attemptContextFactory(); + MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.empty()); + cleanup = new TransactionsCleanup(merged, clusterData); + + config.persistentLoggingCollection().ifPresent(collection -> { + PersistedLogWriter persistedLogWriter = new PersistedLogWriter(collection, MAX_LOG_ENTRIES_DEFAULT); + persistedLogger = new EventBusPersistedLogger(cluster.environment().eventBus(), persistedLogWriter, merged); + }); + } + + + /** + * The main transactions 'engine', responsible for attempting the transaction logic as many times as required, + * until the transaction commits, is explicitly rolled back, or expires. + */ + // TODO: changed from private to public. package-protected plus an accessor would be ok to + public Mono executeTransaction(MergedTransactionConfig config, + TransactionContext overall, + Mono transactionLogic) { + AtomicReference startTime = new AtomicReference<>(); + + return Mono.just(overall) + + .subscribeOn(reactor.core.scheduler.Schedulers.elastic()) + + .doOnSubscribe(v -> { + if (startTime.get() == null) startTime.set(System.nanoTime()); + }) + + // Where the magic happens: execute the app's transaction logic + // A AttemptContextReactive gets created in here. Rollback requires one of these (so it knows what + // to rollback), so only errors thrown inside this block can trigger rollback. + // So, expiry checks only get done inside this block. + .then(transactionLogic) + + .flatMap(this::executeImplicitCommit) + + // Track an attempt if non-error, and request that the attempt be cleaned up. Similar logic is also + // done in executeHandleErrorsPreRetry. + .doOnNext(ctx -> executeAddAttemptAndCleanupRequest(config, overall, ctx)) + + // Track an attempt if error, and perform rollback if needed. + // All errors reaching here must be a `TransactionOperationFailed`. + .onErrorResume(err -> executeHandleErrorsPreRetry(config, overall, err)) + + // This is the main place to retry txns. Feed all errors up to this centralised point. + // All errors reaching here must be a `TransactionOperationFailed`. + .retryWhen(executeCreateRetryWhen(overall)) + + // If we're here, then we've hit an error that we don't want to retry. + // Either raise some derivative of TransactionFailed to the app, or return an AttemptContextReactive + // to return success (some errors result in success, e.g. TRANSACTION_FAILED_POST_COMMIT) + // All errors reaching here must be an `ErrorWrapper`. + .onErrorResume(err -> executeHandleErrorsPostRetry(overall, err)) + + .doOnError(err -> { + if (config.logOnFailure() && !config.logDirectly()) { + EventBus eventBus = cleanup.clusterData().cluster().environment().eventBus(); + overall.LOGGER.logs().forEach(log -> { + eventBus.publish(new TransactionLogEvent(config.logOnFailureLevel(), + TransactionLogEvent.DEFAULT_CATEGORY, log.toString())); + }); + } + }) + + // If we get here, success + .doOnSuccess(v -> + overall.LOGGER.info("finished txn in %dus", + TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startTime.get())) + ) + + // Safe to do single() as there will only ever be 1 result + .single() + .map(v -> createResultFromContext(overall)); + } + + private reactor.util.retry.Retry executeCreateRetryWhen(TransactionContext overall) { + Predicate> predicate = context -> { + Throwable exception = context.exception(); + + if (!(exception instanceof TransactionOperationFailed)) { + // A bug. Only TransactionOperationFailed is allowed to reach here. + throw new IllegalStateException("Non-TransactionOperationFailed '" + DebugUtil.dbg(exception) + "' received during retry, this is a bug", exception); + } + + TransactionOperationFailed e = (TransactionOperationFailed) exception; + + overall.LOGGER.info("TransactionOperationFailed retryTransaction=%s", e.retryTransaction()); + + return e.retryTransaction(); + }; + + return DefaultRetry.create(predicate) + + .exponentialBackoff(Duration.of(1, ChronoUnit.MILLIS), + Duration.of(2, ChronoUnit.MILLIS)) + + .doOnRetry(v -> overall.LOGGER.info("<>", "retrying transaction after backoff %dmillis", v.backoff().toMillis())) + + // Add some jitter so two txns don't livelock each other + .jitter(Jitter.random()) + + // Really, this is a safety-guard. The txn will be aborted when it expires. + .retryMax(MAX_ATTEMPTS) + + .toReactorRetry(); + } + + private Mono executeHandleErrorsPreRetry(MergedTransactionConfig config, + TransactionContext overall, Throwable err) { + if (!(err instanceof TransactionOperationFailed)) { + // A bug. Only TransactionOperationFailed is allowed to reach here. + overall.LOGGER.warn("<>", "received non-TransactionOperationFailed error %s, unable to rollback as don't have " + + "context", DebugUtil.dbg(err)); + return Mono.error(new IllegalStateException("received non-TransactionOperationFailed error " + err.getClass().getName() + " in pre-retry", err)); + } + + Mono autoRollback = Mono.empty(); + Mono cleanupReq = Mono.empty(); + + TransactionOperationFailed e = (TransactionOperationFailed) err; + AttemptContextReactive ctx = e.context(); + + overall.LOGGER.info("<>", "finishing attempt off after error '%s'", e); + + if (e.autoRollbackAttempt()) { + // In queryMode we always ROLLBACK, as there is possibly delta table state to cleanup, and there may be an + // ATR - we don't know + if (ctx.state() == AttemptStates.NOT_STARTED && !ctx.queryMode()) { + // This is a better way of doing [RETRY-ERR-NOATR] and likely means that the older logic for + // handling that won't trigger now + ctx.LOGGER.info(ctx.attemptId(), "told to auto-rollback but in NOT_STARTED state, so nothing to do - skipping rollback"); + } + else { + ctx.LOGGER.info(ctx.attemptId(), "auto-rolling-back on error"); + + autoRollback = ctx.rollbackInternal(false); + } + } else { + ctx.LOGGER.info(ctx.attemptId(), "has been told to skip auto-rollback"); + } + + if (!config.runRegularAttemptsCleanupThread()) { + // Don't add a request to a queue that no-one will be processing + ctx.LOGGER.trace(ctx.attemptId(), "skipping addition of cleanup request on failure as regular cleanup disabled"); + } + else { + cleanupReq = Mono.fromRunnable(() -> addCleanupRequestForContext(ctx)); + } + + Mono addAttempt = Mono.fromRunnable(() -> { + TransactionAttempt ta = TransactionAttempt.createFromContext(ctx, Optional.of(err)); + overall.addAttempt(ta); + ctx.LOGGER.info(ctx.attemptId(), "added attempt %s after error", ta); + }); + + final Mono cleanupReqForLambda = cleanupReq; + + return autoRollback + // See [Primary Operations] section in design document + .onErrorResume(er -> { + overall.LOGGER.info("<>", "rollback failed with %s, raising original error but with retryTransaction turned off", + DebugUtil.dbg(er)); + + // Still want to add attempt and cleanup request + return cleanupReqForLambda + .then(addAttempt) + .then(Mono.error(createError(e.context(), e.causingErrorClass()) + .raiseException(e.toRaise()) + .cause(e.getCause()) + .build())); + }) + .then(cleanupReqForLambda) + // Only want to add the attempt after doing the rollback, so the attempt has the correct state (hopefully + // ROLLED_BACK) + .then(addAttempt) + .then(Mono.defer(() -> { + if (e.retryTransaction() && overall.hasExpiredClientSide()) { + overall.LOGGER.info("<>", "original error planned to retry transaction, but it has subsequently expired"); + return Mono.error(createError(ctx, ErrorClasses.FAIL_EXPIRY) + .doNotRollbackAttempt() + .raiseException(TransactionOperationFailed.FinalErrorToRaise.TRANSACTION_EXPIRED) + .build()); + } + else { + // Raise the error up the stack so the logic later can decide whether to retry the transaction + overall.LOGGER.info("<>", "reraising original exception %s", DebugUtil.dbg(err)); + return Mono.error(err); + } + })) + .doFinally(v -> ctx.span().failWith(e)) + .thenReturn(ctx); + } + + private Mono executeHandleErrorsPostRetry(TransactionContext overall, Throwable err) { + if (!(err instanceof TransactionOperationFailed)) { + // A bug. Only TransactionOperationFailed is allowed to reach here. + return Mono.error(new IllegalStateException("Non-TransactionOperationFailed '" + DebugUtil.dbg(err) + "' received, this is a bug")); + } + + TransactionResult result = createResultFromContext(overall); + TransactionOperationFailed e = (TransactionOperationFailed) err; + + if (e.toRaise() == TransactionOperationFailed.FinalErrorToRaise.TRANSACTION_FAILED_POST_COMMIT) { + e.context().LOGGER.info(e.context().attemptId(), "converted TRANSACTION_FAILED_POST_COMMIT to success, unstagingComplete() will be false"); + + return Mono.just(e.context()); + } + else { + TransactionFailed ret; + + switch (e.toRaise()) { + case TRANSACTION_EXPIRED: { + String msg = "Transaction has expired configured timeout of " + overall.expirationTime().toMillis() + "msecs. The transaction is not committed."; + ret = new TransactionExpired(e.getCause(), result, msg); + break; + } + case TRANSACTION_COMMIT_AMBIGUOUS: { + String msg = "It is ambiguous whether the transaction committed"; + ret = new TransactionCommitAmbiguous(e.getCause(), result, msg); + break; + } + default: + ret = new TransactionFailed(e.getCause(), result); + break; + } + + e.context().LOGGER.info(e.context().attemptId(), "converted TransactionOperationFailed %s to final error %s", + e.toRaise(), ret); + + return Mono.error(ret); + } + } + + private void executeAddAttemptAndCleanupRequest(MergedTransactionConfig config, TransactionContext overall, + AttemptContextReactive ctx) { + TransactionAttempt ta = TransactionAttempt.createFromContext(ctx, Optional.empty()); + overall.addAttempt(ta); + ctx.LOGGER.info(ctx.attemptId(), "added attempt %s after success", ta); + + if (config.runRegularAttemptsCleanupThread()) { + addCleanupRequestForContext(ctx); + } else { + ctx.LOGGER.trace(ctx.attemptId(), "skipping addition of cleanup request on success"); + } + + ctx.span().finish(); + } + + private Mono executeImplicitCommit(AttemptContextReactive ctx) { + return Mono.defer(() -> { + // If app has not explicitly performed a commit, assume they want to do so anyway + if (!ctx.isDone()) { + if (ctx.serialized().isPresent()) { + return Mono.just(ctx); + } else { + ctx.LOGGER.trace(ctx.attemptId(), "doing implicit commit"); + + return ctx.commit() + .then(Mono.just(ctx)) + .onErrorResume(err -> Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx))); + } + } else { + return Mono.just(ctx); + } + }); + } + + // TODO: changed from package-protected to public (could have just used an accessor class in same package) + public AttemptContextReactive createAttemptContext(TransactionContext overall, + MergedTransactionConfig config, + String attemptId) { + // null only happens in testing with Mockito, harmless + if (overall != null) { + return attemptContextFactory.create(overall, config, attemptId, this, Optional.of(overall.span())); + } else { + return null; + } + } + + /** + * Runs the supplied transactional logic until success or failure. + *

+ * This is the asynchronous version of {@link Transactions#run}, so to cover the differences: + *

    + *
  • The transaction logic is supplied with a {@link AttemptContextReactive}, which contains asynchronous + * methods to allow it to read, mutate, insert and delete documents, as well as commit or rollback the + * transactions.
  • + *
  • The transaction logic should run these methods as a Reactor chain.
  • + *
  • The transaction logic should return a Mono{@literal <}Void{@literal >}. Any + * Flux or Mono can be converted to a Mono{@literal <}Void{@literal >} by + * calling .then() on it.
  • + *
  • This method returns a Mono{@literal <}TransactionResult{@literal >}, which should be handled + * as a normal Reactor Mono.
  • + *
+ * + * @param transactionLogic the application's transaction logic + * @param perConfig the configuration to use for this transaction + * @return there is no need to check the returned {@link TransactionResult}, as success is implied by the lack of a + * thrown exception. It contains information useful only for debugging and logging. + * @throws TransactionFailed or a derived exception if the transaction fails to commit for any reason, possibly + * after multiple retries. The exception contains further details of the error. Not + */ + public Mono run(Function> transactionLogic, + PerTransactionConfig perConfig) { + return Mono.defer(() -> { + MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); + + TransactionContext overall = + new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), + cleanup.clusterData().cluster().environment().eventBus(), + UUID.randomUUID().toString(), + now(), + Duration.ZERO, + merged); + AtomicReference startTime = new AtomicReference<>(0L); + + Mono ob = Mono.fromCallable(() -> { + String txnId = UUID.randomUUID().toString(); + overall.LOGGER.info(configDebug(config, perConfig)); + return createAttemptContext(overall, merged, txnId); + }).flatMap(ctx -> { + ctx.LOGGER.info("starting attempt %d/%s/%s", + overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); + Mono result = transactionLogic.apply(ctx); + return result + .onErrorResume(err -> { + ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); + logElidedStacktrace(ctx, err); + + return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); + }) + .thenReturn(ctx); + }).doOnSubscribe(v -> startTime.set(System.nanoTime())) + .doOnNext(v -> v.LOGGER.trace(v.attemptId(), "finished attempt %d in %sms", + overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); + + return executeTransaction(merged, overall, ob) + .doOnNext(v -> overall.span().finish()) + .doOnError(err -> overall.span().failWith(err)); + }); + } + + // Printing the stacktrace is expensive in terms of log noise, but has been a life saver on many debugging + // encounters. Strike a balance by eliding the more useless elements. + // TODO: changed from private to public + public void logElidedStacktrace(AttemptContextReactive ctx, Throwable err) { + DebugUtil.fetchElidedStacktrace(err, (s) -> ctx.LOGGER.info(ctx.attemptId(), " " + s.toString())); + } + + // TODO: changed from private to public + public static String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { + StringBuilder sb = new StringBuilder(); + sb.append("library version: "); + sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); + sb.append(" config: "); + sb.append("atrs="); + sb.append(config.numAtrs()); + sb.append(", metadataCollection="); + sb.append(config.metadataCollection()); + sb.append(", expiry="); + sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); + sb.append("msecs durability="); + sb.append(config.durabilityLevel()); + sb.append(" per-txn config="); + sb.append(" durability="); + sb.append(perConfig.durabilityLevel()); + sb.append(", supported="); + sb.append(Supported.SUPPORTED); + return sb.toString(); + } + + /** + * Convenience overload that runs {@link TransactionsReactive#run} with a default PerTransactionConfig. + */ + public Mono run(Function> transactionLogic) { + return run(transactionLogic, PerTransactionConfigBuilder.create().build()); + } + + @Stability.Volatile + public Mono commit(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { + return deferred(serialized, + perConfig, + // Nothing to actually do, just want the implicit commit + (ctx) -> Mono.empty()); + } + + @Stability.Volatile + public Mono rollback(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { + return deferred(serialized, + perConfig, + (ctx) -> ctx.rollback()); + } + + @Stability.Volatile + private Mono deferred(TransactionSerializedContext serialized, + PerTransactionConfig perConfig, + Function> initial) { + MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); + JsonObject hydrated = JsonObject.fromJson(serialized.encodeAsString()); + + String atrBucket = hydrated.getString("atrBucket"); + String atrScope = hydrated.getString("atrScope"); + String atrCollectionName = hydrated.getString("atrCollection"); + String atrId = hydrated.getString("atrId"); + ReactiveCollection atrCollection = cleanup.clusterData() + .getBucketFromName(atrBucket) + .scope(atrScope) + .collection(atrCollectionName); + + return ActiveTransactionRecord.getAtr(atrCollection, + atrId, + OptionsWrapperUtil.kvTimeoutNonMutating(merged, atrCollection.core()), + null) + + .flatMap(atrOpt -> { + if (!atrOpt.isPresent()) { + return Mono.error(new IllegalStateException(String.format("ATR %s/%s could not be found", + atrBucket, atrId))); + } + else { + ATR atr = atrOpt.get(); + + // Note startTimeServerMillis is written with ${Mutation.CAS} while currentTimeServer + // could have come from $vbucket.HLC and is hence one-second granularity. So, this is a + // somewhat imperfect comparison. + Duration currentTimeServer = Duration.ofNanos(atr.cas()); + Duration startTimeServer = Duration.ofMillis(hydrated.getLong("startTimeServerMillis")); + + // This includes the time elapsed during the first part of the transaction, plus any time + // elapsed during the period the transaction was expired. Total time since the transaction + // began, basically. + Duration timeElapsed = currentTimeServer.minus(startTimeServer); + + TransactionContext overall = + new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), + cleanup.clusterData().cluster().environment().eventBus(), + UUID.randomUUID().toString(), + Duration.ofNanos(System.nanoTime()), + timeElapsed, + merged); + AtomicReference startTime = new AtomicReference<>(0L); + + overall.LOGGER.info("elapsed time = %dmsecs (ATR start time %dmsecs, current ATR time %dmsecs)", + timeElapsed.toMillis(), startTimeServer.toMillis(), currentTimeServer.toMillis()); + + Mono ob = Mono.defer(() -> { + AttemptContextReactive ctx = attemptContextFactory.createFrom(hydrated, overall, merged, this); + ctx.LOGGER.info("starting attempt %d/%s/%s", + overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); + ctx.LOGGER.info(configDebug(config, perConfig)); + + return initial.apply(ctx) + + // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it + .subscribeOn(Schedulers.elastic()) + + .onErrorResume(err -> { + ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in deferred, rethrowing", + err); + + logElidedStacktrace(ctx, err); + + return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); + }) + + .doOnSubscribe(v -> startTime.set(System.nanoTime())) + + .doOnNext(v -> { + ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", + overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); + }) + + .thenReturn(ctx); + }); + + return executeTransaction(merged, overall, ob) + .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) + .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); + } + }); + } + + Mono runBlocking(Consumer txnLogic, PerTransactionConfig perConfig) { + return Mono.defer(() -> { + MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); + TransactionContext overall = + new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), + cleanup.clusterData().cluster().environment().eventBus(), + UUID.randomUUID().toString(), + now(), + Duration.ZERO, + merged); + AtomicReference startTime = new AtomicReference<>(0L); + overall.LOGGER.info(configDebug(config, perConfig)); + + Mono ob = Mono.defer(() -> { + String txnId = UUID.randomUUID().toString(); + AttemptContextReactive ctx = createAttemptContext(overall, merged, txnId); + AttemptContext ctxBlocking = new AttemptContext(ctx); + ctx.LOGGER.info("starting attempt %d/%s/%s", + overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); + + return Mono.fromRunnable(() -> txnLogic.accept(ctxBlocking)) + + // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it + .subscribeOn(Schedulers.elastic()) + + .onErrorResume(err -> { + ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in runBlocking, rethrowing", err); + + logElidedStacktrace(ctx, err); + + return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); + }) + + .doOnSubscribe(v -> startTime.set(System.nanoTime())) + + .doOnNext(v -> { + ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", + overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); + }) + + .thenReturn(ctx); + }); + + return executeTransaction(merged, overall, ob) + .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) + .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); + }); + } + + public TransactionConfig config() { + return config; + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + + TransactionsCleanup cleanup() { + return cleanup; + } + + private void addCleanupRequestForContext(AttemptContextReactive ctx) { + // Whether the txn was successful or not, still want to clean it up + if (ctx.queryMode()) { + ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as in query mode"); + } + else if (ctx.serialized().isPresent()) { + ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as deferred transaction"); + } + else if (ctx.atrId().isPresent() && ctx.atrCollection().isPresent()) { + switch (ctx.state()) { + case NOT_STARTED: + case COMPLETED: + case ROLLED_BACK: + ctx.LOGGER.trace(ctx.attemptId(), "Skipping addition of cleanup request in state %s", ctx.state()); + break; + default: + ctx.LOGGER.trace(ctx.attemptId(), "Adding cleanup request for %s/%s", + ctx.atrCollection().get().name(), ctx.atrId().get()); + + cleanup.add(ctx.createCleanupRequest()); + } + } else { + // No ATR entry to remove + ctx.LOGGER.trace(ctx.attemptId(), "Skipping cleanup request as no ATR entry to remove (due to no " + + "mutations)"); + } + } + + private static TransactionResult createResultFromContext(TransactionContext overall) { + return new TransactionResult(overall.attempts(), + overall.LOGGER, + Duration.of(System.nanoTime() - overall.startTimeClient().toNanos(), ChronoUnit.NANOS), + overall.transactionId(), + overall.serialized()); + } + + /** + * Performs a single query transaction, with default configuration. + * + * @param statement the statement to execute. + * @return a ReactiveSingleQueryTransactionResult + */ + @Stability.Uncommitted + public Mono query(String statement) { + return query(null, statement, SingleQueryTransactionConfigBuilder.create().build()); + } + + /** + * Performs a single query transaction, with a custom configuration. + * + * @param statement the statement to execute. + * @param queryOptions configuration options. + * @return a ReactiveSingleQueryTransactionResult + */ + @Stability.Uncommitted + public Mono query(String statement, SingleQueryTransactionConfig queryOptions) { + return query(null, statement, queryOptions); + } + + /** + * Performs a single query transaction, with a scope context and default configuration. + * + * @param statement the statement to execute. + * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope + * rather than needed to provide the full keyspace. + * @return a ReactiveSingleQueryTransactionResult + */ + @Stability.Uncommitted + public Mono query(ReactiveScope scope, String statement) { + return query(scope, statement, SingleQueryTransactionConfigBuilder.create().build()); + } + + /** + * Performs a single query transaction, with a scope context and custom configuration. + * + * @param statement the statement to execute. + * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope + * rather than needed to provide the full keyspace. + * @param queryOptions configuration options. + * @return a ReactiveSingleQueryTransactionResult + */ + @Stability.Uncommitted + public Mono query(ReactiveScope scope, String statement, SingleQueryTransactionConfig queryOptions) { + return Mono.defer(() -> { + AtomicReference queryResult = new AtomicReference<>(); + return run((ctx) -> ctx.query(scope, statement, queryOptions.queryOptions(), true) + .doOnNext(qr -> queryResult.set(qr)) + .then(), queryOptions.convert()) + .map(result -> new ReactiveSingleQueryTransactionResult(result.log(), queryResult.get())); + }); + } + + @Stability.Internal + @Deprecated // Prefer setting TransactionConfigBuilder#testFactories now + public void setAttemptContextFactory(AttemptContextFactory factory) { + this.attemptContextFactory = factory; + } + public AttemptContextReactive newAttemptContextReactive(){ + PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); + MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); + + TransactionContext overall = new TransactionContext( + cleanup().clusterData().cluster().environment().requestTracer(), + cleanup().clusterData().cluster().environment().eventBus(), + UUID.randomUUID().toString(), now(), Duration.ZERO, merged); + + String txnId = UUID.randomUUID().toString(); + overall.LOGGER.info(configDebug(config, perConfig)); + return createAttemptContext(overall, merged, txnId); + } + +} diff --git a/src/main/java/com/example/demo/CouchbaseTransactionManager.java b/src/main/java/com/example/demo/CouchbaseTransactionManager.java new file mode 100644 index 000000000..41f62bf26 --- /dev/null +++ b/src/main/java/com/example/demo/CouchbaseTransactionManager.java @@ -0,0 +1,209 @@ +package com.example.demo; + +import java.util.concurrent.atomic.AtomicReference; + +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.AttemptContextReactiveAccessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionImpl; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; + +import com.couchbase.transactions.AttemptContext; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; + +public class CouchbaseTransactionManager extends AbstractPlatformTransactionManager + implements DisposableBean, ResourceTransactionManager, CallbackPreferringPlatformTransactionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); + + private final CouchbaseTemplate template; + private final Transactions transactions; + + public CouchbaseTransactionManager(CouchbaseTemplate template, TransactionConfig transactionConfig) { + this.template = template; + this.transactions = Transactions.create( + template.getCouchbaseClientFactory().getCluster(), + transactionConfig + ); + } + + public CouchbaseTransactionalTemplate template() { + return new CouchbaseTransactionalTemplate(template); + } + + @Override + public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { + final AtomicReference result = new AtomicReference<>(); + TransactionResult txnResult = transactions.run(attemptContext -> { + + if (TransactionSynchronizationManager.hasResource(template.getCouchbaseClientFactory())) { + ((CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.reactive().getCouchbaseClientFactory())) + .setAttemptContext(attemptContext); + } else { + TransactionSynchronizationManager.bindResource( + template.reactive().getCouchbaseClientFactory(), + new CouchbaseResourceHolder(attemptContext) + ); + } + + try { + // Since we are on a different thread now transparently, at least make sure + // that the original method invocation is synchronized. + synchronized (this) { + result.set(callback.doInTransaction(null)); + } + } catch (RuntimeException e) { + System.err.println("RuntimeException: "+e+" instanceof RuntimeException "+(e instanceof RuntimeException)); + throw e; + } catch (Throwable e) { + System.err.println("RuntimeException: "+e+" instanceof "+(e instanceof Throwable)); + throw new RuntimeException(e); + } + }); + + LOGGER.debug("Completed Couchbase Transaction with Result: " + txnResult); + return result.get(); + } + + @Override + protected CouchbaseTransactionObject doGetTransaction() throws TransactionException { + CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + return new CouchbaseTransactionObject(resourceHolder); + } + + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractTransaction(transaction).hasResourceHolder(); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + LOGGER.debug("Beginning Couchbase Transaction with Definition {}", definition); + } + + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + LOGGER.debug("Committing Couchbase Transaction with status {}", status); + } + + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + LOGGER.warn("Rolling back Couchbase Transaction with status {}", status); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + LOGGER.trace("Performing cleanup of Couchbase Transaction {}", transaction); + } + + @Override + public void destroy() { + transactions.close(); + } + + @Override + public Object getResourceFactory() { + return template.getCouchbaseClientFactory(); + } + + private static CouchbaseTransactionObject extractTransaction(Object transaction) { + Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + transaction.getClass())); + + return (CouchbaseTransactionObject) transaction; + } + + public static class CouchbaseResourceHolder extends ResourceHolderSupport { + + private volatile AttemptContext attemptContext; + private volatile AttemptContextReactive attemptContextReactive; + private volatile ClientSession session = new ClientSessionImpl(); + + public CouchbaseResourceHolder(AttemptContext attemptContext) { + this.attemptContext = attemptContext; + } + + public AttemptContext getAttemptContext() { + return attemptContext; + } + + public void setAttemptContext(AttemptContext attemptContext) { + this.attemptContext = attemptContext; + } + + public AttemptContextReactive getAttemptContextReactive() { + return attemptContext!= null ? AttemptContextReactiveAccessor.getACR(attemptContext) : attemptContextReactive; + } + public void setAttemptContextReactive(AttemptContextReactive attemptContextReactive) { + this.attemptContextReactive = attemptContextReactive; + } + + public ClientSession getSession() { + return session; + } + + public void setSession(ClientSession session){ + this.session = session; + } + + @Override + public String toString() { + return "CouchbaseResourceHolder{" + + "attemptContext=" + attemptContext + + '}'; + } + + } + + protected static class CouchbaseTransactionObject implements SmartTransactionObject { + + final CouchbaseResourceHolder resourceHolder; + + CouchbaseTransactionObject(CouchbaseResourceHolder resourceHolderIn) { + resourceHolder = resourceHolderIn; + } + + @Override + public boolean isRollbackOnly() { + return resourceHolder != null && resourceHolder.isRollbackOnly(); + } + + @Override + public void flush() { + TransactionSynchronizationUtils.triggerFlush(); + } + + public boolean hasResourceHolder() { + return resourceHolder != null; + } + + @Override + public String toString() { + return "CouchbaseTransactionObject{" + + "resourceHolder=" + resourceHolder + + '}'; + } + } + +} diff --git a/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java new file mode 100644 index 000000000..70116952c --- /dev/null +++ b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java @@ -0,0 +1,78 @@ +package com.example.demo; + +import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import com.couchbase.transactions.AttemptContext; +import com.couchbase.transactions.TransactionGetResult; + +public class CouchbaseTransactionalTemplate { + + private final CouchbaseTemplate template; + + public CouchbaseTransactionalTemplate(CouchbaseTemplate template) { + this.template = template; + } + + public SpringTransactionGetResult findById(String id, Class domainType) { + try { + AttemptContext ctx = getContext(); + TransactionGetResult getResult = ctx.get(template.getCouchbaseClientFactory().getDefaultCollection(), id); + + T t = template.support().decodeEntity(id, getResult.contentAsObject().toString(), getResult.cas(), domainType, + null, null, null); + return new SpringTransactionGetResult<>(t, getResult); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + + } + + public void replaceById(TransactionGetResult getResult, T entity) { + AttemptContext ctx = getContext(); + + ctx.replace(getResult, template.support().encodeEntity(entity).getContent()); + } + + private AttemptContext getContext() { + CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + AttemptContext atr; + if (resource != null) { + atr = resource.getAttemptContext(); + } else { + CouchbaseResourceHolder holder = (CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory().getCluster()); + atr = holder.getSession().getAttemptContext(); + } + return atr; + } + + public static AttemptContextReactive getContextReactive(ReactiveCouchbaseTemplate template) { + CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + AttemptContextReactive atr = null; + if (resource != null) { + atr = resource.getAttemptContextReactive(); + } else { + CouchbaseResourceHolder holder = (CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory().getCluster()); + if (holder != null && holder.getSession() != null) { + atr = holder.getSession().getAttemptContextReactive(); + } + } + return atr; + } + + public static ClientSession getSession(ReactiveCouchbaseTemplate template) { + CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + return resource != null ? resource.getSession() : null; + } + +} diff --git a/src/main/java/com/example/demo/SpringTransactionGetResult.java b/src/main/java/com/example/demo/SpringTransactionGetResult.java new file mode 100644 index 000000000..365751fc9 --- /dev/null +++ b/src/main/java/com/example/demo/SpringTransactionGetResult.java @@ -0,0 +1,30 @@ +package com.example.demo; + +import com.couchbase.transactions.TransactionGetResult; + +public class SpringTransactionGetResult { + + private final T value; + private final TransactionGetResult inner; + + public SpringTransactionGetResult(T value, TransactionGetResult inner) { + this.value = value; + this.inner = inner; + } + + public T getValue() { + return value; + } + + public TransactionGetResult getInner() { + return inner; + } + + @Override + public String toString() { + return "SpringTransactionGetResult{" + + "value=" + value + + ", inner=" + inner + + '}'; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index 51781a7d8..f4203bcf2 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -18,12 +18,18 @@ import java.io.Closeable; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; import org.springframework.dao.support.PersistenceExceptionTranslator; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * The {@link CouchbaseClientFactory} is the main way to get access to the managed SDK instance and resources. @@ -73,4 +79,10 @@ public interface CouchbaseClientFactory extends Closeable { */ PersistenceExceptionTranslator getExceptionTranslator(); + ClientSession getSession(ClientSessionOptions options, Transactions transactions, + TransactionConfig config , AttemptContextReactive atr); + + //CouchbaseClientFactory with(CouchbaseStuffHandle txOp); + + //CouchbaseStuffHandle getTransactionalOperator(); } diff --git a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java new file mode 100644 index 000000000..cfeab6cef --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java @@ -0,0 +1,119 @@ +/* + * Copyright 2016-2021 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; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterInterface; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.Scope; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import reactor.core.publisher.Mono; + +import org.springframework.dao.support.PersistenceExceptionTranslator; + +import java.io.IOException; + + +/** + * Interface for factories creating reactive {@link Cluster} instances. + * + * @author Mark Paluch + * @author Christoph Strobl + * @author Mathieu Ouellet + * @since 2.0 + */ +public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider*/ { + + /** + * Provides access to the managed SDK {@link Cluster} reference. + */ + //Cluster getCluster(); + + Mono getCluster(); + + /** + * Provides access to the managed SDK {@link Bucket} reference. + */ + Mono getBucket(); + + /** + * Provides access to the managed SDK {@link Scope} reference. + */ + //Scope getScope(); + + Mono getScope(); + + /** + * Provides access to a collection (identified by its name) in managed SDK {@link Scope} reference. + * + * @param name the name of the collection. If null is passed in, the default collection is assumed. + */ + //Collection getCollection(String name); + + Mono getCollection(String name); + /** + * Provides access to the default collection. + */ + Mono getDefaultCollection(); + + /** + * Returns a new {@link CouchbaseClientFactory} set to the scope given as an argument. + * + * @param scopeName the name of the scope to use for all collection access. + * @return a new client factory, bound to the other scope. + */ + ReactiveCouchbaseClientFactory withScope(String scopeName); + + /** + * The exception translator used on the factory. + */ + PersistenceExceptionTranslator getExceptionTranslator(); + + Mono getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config); + + String getBucketName(); + + String getScopeName(); + + void close() throws IOException; + + Mono getSession(ClientSessionOptions options); + + ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, + AttemptContextReactive atr); + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.ClientSession) + */ + ReactiveCouchbaseClientFactory withSession(ClientSession session); + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#isTransactionActive() + */ + boolean isTransactionActive(); + + //CouchbaseStuffHandle getTransactionalOperator(); + + //ReactiveCouchbaseClientFactory with(CouchbaseStuffHandle txOp); +} diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index e19243718..f560e09ae 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -15,10 +15,24 @@ */ package org.springframework.data.couchbase; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; import java.util.function.Supplier; +import com.couchbase.transactions.AttemptContext; +import com.couchbase.transactions.AttemptContextReactiveAccessor; +import com.couchbase.transactions.TransactionContext; +import com.couchbase.transactions.config.MergedTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfigBuilder; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionImpl; +import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import com.couchbase.client.core.env.Authenticator; import com.couchbase.client.core.env.OwnedSupplier; @@ -29,6 +43,9 @@ import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; /** * The default implementation of a {@link CouchbaseClientFactory}. @@ -68,10 +85,16 @@ public SimpleCouchbaseClientFactory(final Cluster cluster, final String bucketNa private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, final String scopeName) { + this(cluster, bucketName, scopeName, null); + } + + private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, final String scopeName, + final CouchbaseStuffHandle transactionalOperator) { this.cluster = cluster; this.bucket = cluster.get().bucket(bucketName); this.scope = scopeName == null ? bucket.defaultScope() : bucket.scope(scopeName); this.exceptionTranslator = new CouchbaseExceptionTranslator(); + //this.transactionalOperator = transactionalOperator; } @Override @@ -116,6 +139,27 @@ public PersistenceExceptionTranslator getExceptionTranslator() { return exceptionTranslator; } + @Override + public ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, + AttemptContextReactive atr) { + // can't we just use AttemptContextReactive everywhere? Instead of creating AttemptContext(atr), then + // accessing at.getACR() ? + AttemptContext at = AttemptContextReactiveAccessor.from( atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive())); + + return new ClientSessionImpl(this, transactions, config, at); + } + + + //@Override + //public CouchbaseClientFactory with(CouchbaseStuffHandle txOp) { + // return new SimpleCouchbaseClientFactory(cluster, bucket.name(), scope.name(), txOp); + //} + + //@Override + //public CouchbaseStuffHandle getTransactionalOperator() { + // return (CouchbaseStuffHandle) transactionalOperator; + //} + @Override public void close() { if (cluster instanceof OwnedSupplier) { @@ -123,4 +167,8 @@ public void close() { } } + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + } diff --git a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java new file mode 100644 index 000000000..46159bf33 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java @@ -0,0 +1,361 @@ +package org.springframework.data.couchbase; + +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; + +import com.couchbase.client.java.ClusterInterface; +import com.couchbase.transactions.AttemptContextReactiveAccessor; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import reactor.core.publisher.Mono; + +import java.io.IOException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionImpl; +import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.data.couchbase.transaction.SessionAwareMethodInterceptor; +import org.springframework.util.ObjectUtils; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.Scope; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; + +public class SimpleReactiveCouchbaseClientFactory implements ReactiveCouchbaseClientFactory { + final Mono cluster; + final String bucketName; + final String scopeName; + final PersistenceExceptionTranslator exceptionTranslator; + //CouchbaseStuffHandle transactionalOperator; + + public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName) { + this(cluster, bucketName, scopeName, null); + } + + public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName, + CouchbaseStuffHandle stuff) { + this.cluster = Mono.just(cluster); + this.bucketName = bucketName; + this.scopeName = scopeName; + this.exceptionTranslator = new CouchbaseExceptionTranslator(); + //this.transactionalOperator = stuff; + } + + @Override + public Mono getCluster() { + return cluster; + } + + @Override + public Mono getBucket() { + return cluster.map((c) -> c.bucket(bucketName)); + } + + @Override + public String getBucketName() { + return bucketName; + } + + @Override + public Mono getScope() { + return cluster.map((c) -> c.bucket(bucketName).scope(scopeName != null ? scopeName : DEFAULT_SCOPE)); + } + + @Override + public String getScopeName() { + return scopeName; + } + + @Override + public Mono getCollection(String collectionName) { + if (getScopeName() != null && !DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName == null || DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used."); + } + } + if (getScopeName() == null || DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName != null && !DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException( + "A collectionName must be null or " + DEFAULT_COLLECTION + " if scope is null or " + DEFAULT_SCOPE); + } + } + return getScope().map((s) -> s.collection(collectionName != null ? collectionName : DEFAULT_COLLECTION)); + } + + @Override + public Mono getDefaultCollection() { + if (getScopeName() != null && DEFAULT_SCOPE.equals(getScopeName())) { + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used."); + } + return cluster.map((c) -> c.bucket(bucketName).defaultCollection()); + } + + @Override + public ReactiveCouchbaseClientFactory withScope(String scopeName) { + return new SimpleReactiveCouchbaseClientFactory((Cluster) cluster.block(), bucketName, + scopeName != null ? scopeName : this.scopeName); + } + + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return exceptionTranslator; + } + + @Override + public Mono getSession(ClientSessionOptions options, Transactions transactions, + TransactionConfig config /*, AttemptContextReactive atr*/) { + throw new RuntimeException("TODO: maybe not used"); + // return Mono.just(new ClientSessionImpl(this, transactions, config)); + } + + @Override + public void close() { + cluster.block().disconnect(); + } + + @Override + public Mono getSession(ClientSessionOptions options) { // hopefully this gets filled in later + return Mono.from(Mono.just(new ClientSessionImpl(this, null, null, null))); // .startSession(options)); + } + + @Override + public ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, + AttemptContextReactive atr) { + + AttemptContextReactive at = atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive()); + + return new ClientSessionImpl(this, transactions, config, at); + } + + @Override + public ReactiveCouchbaseClientFactory withSession(ClientSession session) { + return new ClientSessionBoundCouchbaseClientFactory(session, this); + } + + @Override + public boolean isTransactionActive() { + return false; + } + + //@Override + //public CouchbaseStuffHandle getTransactionalOperator() { + // return transactionalOperator; + //} + + //@Override + //public ReactiveCouchbaseClientFactory with(CouchbaseStuffHandle txOp) { + // return new SimpleReactiveCouchbaseClientFactory((Cluster) getCluster().block(), bucketName, scopeName, txOp); + //} + + private T createProxyInstance(ClientSession session, T target, Class targetType) { + + ProxyFactory factory = new ProxyFactory(); + factory.setTarget(target); + factory.setInterfaces(targetType); + factory.setOpaque(true); + + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, ClusterInterface.class, + this::proxyDatabase, Collection.class, this::proxyCollection)); + + return targetType.cast(factory.getProxy(target.getClass().getClassLoader())); + } + + private Collection proxyCollection(ClientSession session, Collection c) { + return createProxyInstance(session, c, Collection.class); + } + + private ClusterInterface proxyDatabase(ClientSession session, ClusterInterface cluster) { + return createProxyInstance(session, cluster, ClusterInterface.class); + } + + /** + * {@link ClientSession} bound TODO decorating the database with a {@link SessionAwareMethodInterceptor}. + * + * @author Christoph Strobl + * @since 2.1 + */ + static final class ClientSessionBoundCouchbaseClientFactory implements ReactiveCouchbaseClientFactory { + + private final ClientSession session; + private final ReactiveCouchbaseClientFactory delegate; + + ClientSessionBoundCouchbaseClientFactory(ClientSession session, ReactiveCouchbaseClientFactory delegate) { + this.session = session; + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getMongoDatabase() + */ + @Override + public Mono getCluster() throws DataAccessException { + return delegate.getCluster().map(this::decorateDatabase); + } + + @Override + public Mono getBucket() { + return delegate.getBucket(); + } + + @Override + public Mono getScope() { + return delegate.getScope(); + } + + @Override + public Mono getCollection(String name) { + return delegate.getCollection(name); + } + + @Override + public Mono getDefaultCollection() { + return delegate.getDefaultCollection(); + } + + @Override + public ReactiveCouchbaseClientFactory withScope(String scopeName) { + return delegate.withScope(scopeName); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getExceptionTranslator() + */ + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return delegate.getExceptionTranslator(); + } + + @Override + public Mono getSession(ClientSessionOptions options, Transactions transactions, + TransactionConfig config) { + return Mono.just(session); + } + + @Override + public String getBucketName() { + return delegate.getBucketName(); + } + + @Override + public String getScopeName() { + return delegate.getScopeName(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getSession(com.mongodb.ClientSessionOptions) + */ + @Override + public Mono getSession(ClientSessionOptions options) { + return getSession(options, null, null); + } + + @Override + public ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, + AttemptContextReactive atr) { + return delegate.getSession(options, transactions, config, atr); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.ClientSession) + */ + @Override + public ReactiveCouchbaseClientFactory withSession(ClientSession session) { + return delegate.withSession(session); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#isTransactionActive() + */ + @Override + public boolean isTransactionActive() { + return session != null && session.hasActiveTransaction(); + } + + //@Override + //public CouchbaseStuffHandle getTransactionalOperator() { + // return delegate.getTransactionalOperator(); + //} + + //@Override + //public ReactiveCouchbaseClientFactory with(CouchbaseStuffHandle txOp) { + // return delegate.with(txOp); + //} + + private ClusterInterface decorateDatabase(ClusterInterface database) { + return createProxyInstance(session, database, ClusterInterface.class); + } + + private ClusterInterface proxyDatabase(ClientSession session, ClusterInterface database) { + return createProxyInstance(session, database, ClusterInterface.class); + } + + private Collection proxyCollection(ClientSession session, Collection collection) { + return createProxyInstance(session, collection, Collection.class); + } + + private T createProxyInstance(ClientSession session, T target, Class targetType) { + + ProxyFactory factory = new ProxyFactory(); + factory.setTarget(target); + factory.setInterfaces(targetType); + factory.setOpaque(true); + + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, + ClusterInterface.class, this::proxyDatabase, Collection.class, this::proxyCollection)); + + return targetType.cast(factory.getProxy(target.getClass().getClassLoader())); + } + + public ClientSession getSession() { + return this.session; + } + + public ReactiveCouchbaseClientFactory getDelegate() { + return this.delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + ClientSessionBoundCouchbaseClientFactory that = (ClientSessionBoundCouchbaseClientFactory) o; + + if (!ObjectUtils.nullSafeEquals(this.session, that.session)) { + return false; + } + return ObjectUtils.nullSafeEquals(this.delegate, that.delegate); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(this.session); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.delegate); + return result; + } + + public String toString() { + return "SimpleReactiveCouchbaseDatabaseFactory.ClientSessionBoundCouchDbFactory(session=" + this.getSession() + + ", delegate=" + this.getDelegate() + ")"; + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 84146860d..127044c1d 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -33,7 +33,9 @@ import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; +import org.springframework.data.couchbase.SimpleReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; @@ -44,6 +46,9 @@ import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; @@ -136,10 +141,16 @@ public CouchbaseClientFactory couchbaseClientFactory(final Cluster couchbaseClus return new SimpleCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); } + @Bean + public ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory(final Cluster couchbaseCluster) { + return new SimpleReactiveCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); + } + @Bean(destroyMethod = "disconnect") public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment) { - return Cluster.connect(getConnectionString(), + Cluster c = Cluster.connect(getConnectionString(), clusterOptions(authenticator()).environment(couchbaseClusterEnvironment)); + return c; } @Bean(destroyMethod = "shutdown") @@ -164,26 +175,31 @@ protected void configureEnvironment(final ClusterEnvironment.Builder builder) { @Bean(name = BeanNames.COUCHBASE_TEMPLATE) public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, - getDefaultConsistency()); + return new CouchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, + couchbaseTranslationService, getDefaultConsistency()); } public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter) { - return couchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); + return couchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, + new JacksonTranslationService()); } @Bean(name = BeanNames.REACTIVE_COUCHBASE_TEMPLATE) - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, - getDefaultConsistency()); + return new ReactiveCouchbaseTemplate(reactiveCouchbaseClientFactory, mappingCouchbaseConverter, + couchbaseTranslationService, getDefaultConsistency()); } - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter) { - return reactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, + return reactiveCouchbaseTemplate( reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); } @@ -319,13 +335,39 @@ public ObjectMapper couchbaseObjectMapper() { } @Bean(COUCHBASE_TRANSACTIONS) - public Transactions getTransactions(Cluster cluster) { - return Transactions.create(cluster, getTransactionConfig()); + public Transactions getTransactions(Cluster cluster, TransactionConfig transactionConfig) { + return Transactions.create(cluster, transactionConfig); } - TransactionConfig getTransactionConfig() { + @Bean + public TransactionConfig transactionConfig() { return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.NONE).build(); + .expirationTime(Duration.ofSeconds(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); + } + + @Bean(BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + ReactiveCouchbaseTransactionManager reactiveTransactionManager( + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, Transactions transactions) { + return new ReactiveCouchbaseTransactionManager(reactiveCouchbaseClientFactory, transactions); + } + + @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) + CouchbaseTransactionManager transactionManager(CouchbaseClientFactory couchbaseClientFactory, + Transactions transactions) { + return new CouchbaseTransactionManager(couchbaseClientFactory, transactions); + } + + /** + * Blocking Transaction Manager + * + * @param couchbaseTemplate + * @param transactionConfig + * @return + */ + @Bean(BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) + CouchbaseCallbackTransactionManager callbackTransactionManager(CouchbaseTemplate couchbaseTemplate, ReactiveCouchbaseTemplate couchbaseReactiveTemplate, + TransactionConfig transactionConfig) { + return new CouchbaseCallbackTransactionManager(couchbaseTemplate, couchbaseReactiveTemplate, transactionConfig); } /** diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index 30040cb20..f462c0480 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -64,4 +64,9 @@ public class BeanNames { public static final String COUCHBASE_CLIENT_FACTORY = "couchbaseClientFactory"; + public static final String REACTIVE_COUCHBASE_TRANSACTION_MANAGER = "reactiveCouchbaseTransactionManager"; + + public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager"; + + public static final String COUCHBASE_CALLBACK_TRANSACTION_MANAGER = "couchbaseCallbackTransactionManager"; } diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 10388e290..0ecfc2825 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -1,3 +1,18 @@ +/* + * Copyright 2021 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.core; import org.slf4j.Logger; @@ -13,6 +28,7 @@ import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -41,9 +57,8 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv abstract ReactiveCouchbaseTemplate getReactiveTemplate(); - public T decodeEntityBase(String id, String source, long cas, Class entityClass, - String scope, String collection, TransactionResultHolder txResultHolder) { - + public T decodeEntityBase(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ClientSession session) { final CouchbaseDocument converted = new CouchbaseDocument(id); converted.setId(id); @@ -90,7 +105,15 @@ public T decodeEntityBase(String id, String source, long cas, Class entit if (cas != 0 && persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id, scope, collection); + if (persistentEntity.transactionResultProperty() != null) { + accessor.setProperty(persistentEntity.transactionResultProperty(), System.identityHashCode(txResultHolder)); + } + N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id, scope, collection); + + if(session != null){ + session.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + } + return accessor.getBean(); } @@ -104,7 +127,7 @@ CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder) { + TransactionResultHolder txResultHolder, ClientSession session) { ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() @@ -122,7 +145,10 @@ public T applyResultBase(T entity, CouchbaseDocument converted, Object id, l final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty(); if (transactionResultProperty != null) { - accessor.setProperty(transactionResultProperty, txResultHolder); + accessor.setProperty(transactionResultProperty, System.identityHashCode(txResultHolder)); + } + if(session != null){ + session.transactionResultHolder(txResultHolder, (T)accessor.getBean()); } maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted)); return (T) accessor.getBean(); @@ -156,14 +182,14 @@ ConvertingPropertyAccessor getPropertyAccessor(final T source) { return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); } - public TransactionResultHolder getTxResultHolder(T source) { + public Integer getTxResultKey(T source) { final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(source.getClass()); final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty(); if (transactionResultProperty == null) { throw new CouchbaseException("the entity class " + source.getClass() + " does not have a property required for transactions:\n\t@TransactionResult TransactionResultHolder txResultHolder"); } - return getPropertyAccessor(source).getProperty(transactionResultProperty, TransactionResultHolder.class); + return getPropertyAccessor(source).getProperty(transactionResultProperty, Integer.class); } public void maybeEmitEvent(CouchbaseMappingEvent event) { @@ -183,4 +209,8 @@ public void maybeEmitEvent(CouchbaseMappingEvent event) { private boolean canPublishEvent() { return this.applicationContext != null; } + + public TranslationService getTranslationService(){ + return translationService; + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index 33ce0791a..d0c2b3370 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -18,6 +18,9 @@ import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; + +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; import com.couchbase.client.java.query.QueryScanConsistency; @@ -50,4 +53,8 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations { * Returns the default consistency to use for queries */ QueryScanConsistency getConsistency(); + T save(T entity); + + Long count(Query query, Class domainType); + } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 4bf781210..674b2037e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -16,11 +16,14 @@ package org.springframework.data.couchbase.core; +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; @@ -28,6 +31,9 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -49,26 +55,49 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex private final CouchbaseTemplateSupport templateSupport; private final MappingContext, CouchbasePersistentProperty> mappingContext; private final ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + private final QueryScanConsistency scanConsistency; private @Nullable CouchbasePersistentEntityIndexCreator indexCreator; - private QueryScanConsistency scanConsistency; - - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { - this(clientFactory, converter, new JacksonTranslationService()); + private CouchbaseStuffHandle txOp; + + //public CouchbaseTemplate with(CouchbaseStuffHandle transactionalOperator) { + // CouchbaseTemplate tmpl = new CouchbaseTemplate(getCouchbaseClientFactory(), + // reactiveCouchbaseTemplate.getCouchbaseClientFactory(), getConverter()); + // tmpl.txOp = transactionalOperator; + // return this; + //} + + /* + public CouchbaseTemplate with(CouchbaseTransactionalOperatorNonReactive transactionalOperator) { + CouchbaseTemplate tmpl = new CouchbaseTemplate(getCouchbaseClientFactory(), getConverter()); + tmpl.txOp = transactionalOperator; + return this; + } + */ + //public CouchbaseStuffHandle txOperator() { + // return txOp; + //} + + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter) { + this(clientFactory, reactiveCouchbaseClientFactory, converter, new JacksonTranslationService()); } - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, CouchbaseConverter converter, final TranslationService translationService) { - this(clientFactory, converter, translationService, null); + this(clientFactory, reactiveCouchbaseClientFactory, converter, translationService, null); } - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter, final TranslationService translationService, QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.templateSupport = new CouchbaseTemplateSupport(this, converter, translationService); - this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService, - scanConsistency); + this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(reactiveCouchbaseClientFactory, converter, + translationService, scanConsistency); this.scanConsistency = scanConsistency; + this.mappingContext = this.converter.getMappingContext(); if (mappingContext instanceof CouchbaseMappingContext) { CouchbaseMappingContext cmc = (CouchbaseMappingContext) mappingContext; @@ -78,6 +107,20 @@ public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final Couch } } + public T save(T entity) { + if (hasNonZeroVersionProperty(entity, templateSupport.converter)) { + return replaceById((Class) entity.getClass()).one(entity); + //} else if (getTransactionalOperator() != null) { + // return insertById((Class) entity.getClass()).one(entity); + } else { + return upsertById((Class) entity.getClass()).one(entity); + } + } + + public Long count(Query query, Class domainType) { + return findByQuery(domainType).matching(query).count(); + } + @Override public ExecutableUpsertById upsertById(final Class domainType) { return new ExecutableUpsertByIdOperationSupport(this).upsertById(domainType); @@ -210,4 +253,24 @@ public TemplateSupport support() { return templateSupport; } + /** + * Get the TransactionalOperator from
+ * 1. The template.clientFactory
+ * 2. The template.threadLocal
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() + *//* + private CouchbaseStuffHandle getTransactionalOperator() { + if (this.getCouchbaseClientFactory().getTransactionalOperator() != null) { + return this.getCouchbaseClientFactory().getTransactionalOperator(); + } + ReactiveCouchbaseTemplate t = this.reactive(); + PseudoArgs pArgs = t.getPseudoArgs(); + if (pArgs != null && pArgs.getTxOp() != null) { + return pArgs.getTxOp(); + } + return null; + } + */ } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 1c1ef0920..2bd616368 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -27,6 +27,7 @@ import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.util.Assert; @@ -67,17 +68,35 @@ ReactiveCouchbaseTemplate getReactiveTemplate() { } @Override - public T decodeEntity(String id, String source, long cas, Class entityClass, - String scope, String collection, TransactionResultHolder txHolder) { - return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder); + public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txHolder); + } + + @Override + public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txHolder, ClientSession session) { + return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, session); } @Override public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder) { - return applyResultBase(entity, converted, id, cas, txResultHolder); + TransactionResultHolder txResultHolder) { + return applyResult(entity, converted, id, cas,txResultHolder, null); } + @Override + public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder, ClientSession session) { + return applyResultBase(entity, converted, id, cas, txResultHolder, session); + } + + @Override + public Integer getTxResultHolder(T source) { + return null; + } + + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index 7dec07692..affea0d8b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -27,7 +27,7 @@ import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * Get Operations @@ -118,7 +118,7 @@ interface FindByIdWithTransaction extends TerminatingFindById, WithTransac * @throws IllegalArgumentException if field is {@literal null}. */ @Override - FindByIdWithProjection transaction(AttemptContextReactive txCtx); + FindByIdWithProjection transaction(CouchbaseStuffHandle txCtx); } interface FindByIdTxOrNot extends FindByIdWithExpiry, FindByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 4d16ffb00..aedd2f3ec 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -21,10 +21,13 @@ import java.util.List; import org.springframework.data.couchbase.core.ReactiveFindByIdOperationSupport.ReactiveFindByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; import org.springframework.util.Assert; import com.couchbase.client.java.kv.GetOptions; -import com.couchbase.transactions.AttemptContextReactive; +import reactor.core.publisher.Mono; public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOperation { @@ -48,11 +51,11 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final GetOptions options; private final List fields; private final Duration expiry; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, - GetOptions options, List fields, Duration expiry, AttemptContextReactive txCtx) { + GetOptions options, List fields, Duration expiry, CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -67,7 +70,9 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { @Override public T one(final String id) { - return reactiveSupport.one(id).block(); + //Mono.deferContextual(ctx -> { System.err.println("ExecutableFindById.ctx: "+ctx); return Mono.empty();}).block(); + return reactiveSupport.one(id)/*.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder())*/.block(); } @Override @@ -104,7 +109,7 @@ public FindByIdWithProjection withExpiry(final Duration expiry) { } @Override - public FindByIdWithExpiry transaction(AttemptContextReactive txCtx) { + public FindByIdWithExpiry transaction(CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index 245cbaecc..edabda276 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -30,11 +30,11 @@ import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; import org.springframework.data.couchbase.core.support.WithTransaction; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.lang.Nullable; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.transactions.AttemptContextReactive; /** * Query Operations @@ -193,7 +193,7 @@ interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithT * @throws IllegalArgumentException if field is {@literal null}. */ @Override - TerminatingFindByQuery transaction(AttemptContextReactive txCtx); + TerminatingFindByQuery transaction(CouchbaseStuffHandle txCtx); } interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index d06d522ec..5bc7c1ba9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -20,11 +20,11 @@ import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.transactions.AttemptContextReactive; /** * {@link ExecutableFindByQueryOperation} implementations for Couchbase. @@ -61,12 +61,12 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final QueryOptions options; private final String[] distinctFields; private final String[] fields; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, - final AttemptContextReactive txCtx) { + final CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -151,7 +151,7 @@ public FindByQueryWithProjecting distinct(final String[] distinctFields) { } @Override - public FindByQueryWithDistinct transaction(AttemptContextReactive txCtx) { + public FindByQueryWithDistinct transaction(CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null!"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, fields, txCtx); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index 48f4ba8e6..4427e946b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -30,7 +30,7 @@ import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * Insert Operations @@ -103,7 +103,7 @@ interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpir interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { @Override - InsertByIdWithExpiry transaction(AttemptContextReactive txCtx); + InsertByIdWithExpiry transaction(CouchbaseStuffHandle txCtx); } interface InsertByIdTxOrNot extends InsertByIdWithExpiry, InsertByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index a03d481d1..0a5181f4b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -19,13 +19,13 @@ import java.util.Collection; import org.springframework.data.couchbase.core.ReactiveInsertByIdOperationSupport.ReactiveInsertByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; public class ExecutableInsertByIdOperationSupport implements ExecutableInsertByIdOperation { @@ -53,12 +53,12 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final AttemptContextReactive txCtx) { + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -126,7 +126,7 @@ public InsertByIdWithDurability withExpiry(final Duration expiry) { } @Override - public InsertByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + public InsertByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 1c650d9ce..9bd423912 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -30,7 +30,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * Remove Operations on KV service. @@ -105,7 +105,7 @@ interface RemoveByIdWithCas extends RemoveByIdWithDurability, WithCas { @Override - TerminatingRemoveById transaction(AttemptContextReactive txCtx); + TerminatingRemoveById transaction(CouchbaseStuffHandle txCtx); } interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index 0176e9493..b777948fe 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -19,13 +19,13 @@ import java.util.List; import org.springframework.data.couchbase.core.ReactiveRemoveByIdOperationSupport.ReactiveRemoveByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByIdOperation { @@ -58,12 +58,12 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas, AttemptContextReactive txCtx) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas, CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -129,7 +129,7 @@ public RemoveByIdWithDurability withCas(Long cas) { } @Override - public RemoveByIdWithCas transaction(AttemptContextReactive txCtx) { + public RemoveByIdWithCas transaction(CouchbaseStuffHandle txCtx) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index ede240095..f7cf8950e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -28,7 +28,7 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * RemoveBy Query Operations @@ -92,7 +92,7 @@ interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith */ interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { @Override - TerminatingRemoveByQuery transaction(AttemptContextReactive txCtx); + TerminatingRemoveByQuery transaction(CouchbaseStuffHandle txCtx); } interface RemoveByQueryWithTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index 3e9497830..55148ee7b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -19,11 +19,11 @@ import org.springframework.data.couchbase.core.ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.transactions.AttemptContextReactive; public class ExecutableRemoveByQueryOperationSupport implements ExecutableRemoveByQueryOperation { @@ -50,11 +50,11 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final String scope; private final String collection; private final QueryOptions options; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, - AttemptContextReactive txCtx) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.query = query; @@ -111,10 +111,11 @@ public RemoveByQueryInCollection inScope(final String scope) { } @Override - public TerminatingRemoveByQuery transaction(final AttemptContextReactive txCtx) { + public TerminatingRemoveByQuery transaction(final CouchbaseStuffHandle txCtx) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options, txCtx); } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index f16d4e623..5ca87b975 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -30,7 +30,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * Replace Operations @@ -101,7 +101,7 @@ interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExp interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { @Override - TerminatingReplaceById transaction(AttemptContextReactive txCtx); + TerminatingReplaceById transaction(CouchbaseStuffHandle txCtx); } interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index 4427df94a..c65905670 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -19,13 +19,13 @@ import java.util.Collection; import org.springframework.data.couchbase.core.ReactiveReplaceByIdOperationSupport.ReactiveReplaceByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; public class ExecutableReplaceByIdOperationSupport implements ExecutableReplaceByIdOperation { @@ -53,12 +53,12 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final AttemptContextReactive txCtx) { + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -113,7 +113,7 @@ public ReplaceByIdWithDurability withExpiry(final Duration expiry) { } @Override - public ReplaceByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + public ReplaceByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx); diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index 1a41ee846..538141952 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -15,10 +15,11 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.transaction.ClientSession; import reactor.core.publisher.Mono; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; /** @@ -42,16 +43,29 @@ public Mono encodeEntity(Object entityToEncode) { @Override public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder) { - return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope , collection, txResultHolder)); + TransactionResultHolder txResultHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder) { + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ClientSession session) { + return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, session)); + } + + @Override + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder) { return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder)); } + @Override + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, ClientSession session) { + return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder, session)); + } + + @Override public Long getCas(Object entity) { return support.getCas(entity); @@ -63,7 +77,12 @@ public String getJavaNameForEntity(Class clazz) { } @Override - public TransactionResultHolder getTxResultHolder(T source) { + public Integer getTxResultHolder(T source) { return support.getTxResultHolder(source); } + + @Override + public TranslationService getTranslationService() { + return support.getTranslationService(); + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java index 81b8cfdef..25d39e859 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java @@ -16,7 +16,10 @@ package org.springframework.data.couchbase.core; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; +import reactor.core.publisher.Mono; import com.couchbase.client.java.query.QueryScanConsistency; @@ -47,10 +50,14 @@ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOper /** * Returns the underlying client factory. */ - CouchbaseClientFactory getCouchbaseClientFactory(); + ReactiveCouchbaseClientFactory getCouchbaseClientFactory(); - /** - * @return the default consistency to use for queries - */ - QueryScanConsistency getConsistency(); + Mono save(T entity); + + Mono count(Query query, Class personClass); + + /** + * @return the default consistency to use for queries + */ + QueryScanConsistency getConsistency(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index a120b5280..f0c643a26 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -16,6 +16,16 @@ package org.springframework.data.couchbase.core; +import com.couchbase.client.java.ClusterInterface; +import org.springframework.context.ApplicationListener; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseClientUtils; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.transaction.SessionSynchronization; +import org.springframework.data.mapping.context.MappingContextEvent; +import reactor.core.publisher.Mono; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -26,10 +36,15 @@ import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import com.couchbase.client.java.Collection; import com.couchbase.client.java.query.QueryScanConsistency; +import java.util.function.Consumer; + +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; + /** * template class for Reactive Couchbase operations * @@ -40,24 +55,31 @@ */ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, ApplicationContextAware { - private final CouchbaseClientFactory clientFactory; + private final ReactiveCouchbaseClientFactory clientFactory; private final CouchbaseConverter converter; private final PersistenceExceptionTranslator exceptionTranslator; private final ReactiveCouchbaseTemplateSupport templateSupport; private ThreadLocal> threadLocalArgs = new ThreadLocal<>(); private QueryScanConsistency scanConsistency; - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { - this(clientFactory, converter, new JacksonTranslationService()); - } + //public ReactiveCouchbaseTemplate with(CouchbaseStuffHandle txOp) { + // TODO: why does txOp go on the clientFactory? can't we just put it on the template?? + // return new ReactiveCouchbaseTemplate(getCouchbaseClientFactory().with(txOp), getConverter(), + // support().getTranslationService(), getConsistency()); + //} + + //public CouchbaseStuffHandle txOperator() { + // return clientFactory.getTransactionalOperator(); + //} - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService) { - this(clientFactory, converter, translationService, null); + public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, + final CouchbaseConverter converter) { + this(clientFactory, converter, new JacksonTranslationService(), null); } - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService, QueryScanConsistency scanConsistency) { + public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, + final CouchbaseConverter converter, final TranslationService translationService, + final QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); @@ -65,6 +87,33 @@ public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, fin this.scanConsistency = scanConsistency; } + // public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { + // this(clientFactory, converter, new JacksonTranslationService()); + // } + + // public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, final CouchbaseConverter + // converter, + // final TranslationService translationService) { + // this.clientFactory = clientFactory; + // this.converter = converter; + // this.exceptionTranslator = this.clientFactory.getExceptionTranslator(); + // this.templateSupport = new ReactiveCouchbaseTemplateSupport(this, converter, translationService); + // } + + public Mono save(T entity) { + if (hasNonZeroVersionProperty(entity, templateSupport.converter)) { + return replaceById((Class) entity.getClass()).one(entity); + //} else if (getTransactionalOperator() != null) { + // return insertById((Class) entity.getClass()).one(entity); + } else { + return upsertById((Class) entity.getClass()).one(entity); + } + } + + public Mono count(Query query, Class domainType) { + return findByQuery(domainType).matching(query).all().count(); + } + @Override public ReactiveFindById findById(Class domainType) { return new ReactiveFindByIdOperationSupport(this).findById(domainType); @@ -127,16 +176,16 @@ public ReactiveUpsertById upsertById(Class domainType) { @Override public String getBucketName() { - return clientFactory.getBucket().name(); + return clientFactory.getBucket().block().name(); } @Override public String getScopeName() { - return clientFactory.getScope().name(); + return clientFactory.getScope().block().name(); } @Override - public CouchbaseClientFactory getCouchbaseClientFactory() { + public ReactiveCouchbaseClientFactory getCouchbaseClientFactory() { return clientFactory; } @@ -147,7 +196,7 @@ public CouchbaseClientFactory getCouchbaseClientFactory() { * @return the collection instance. */ public Collection getCollection(final String collectionName) { - return clientFactory.getCollection(collectionName); + return clientFactory.getCollection(collectionName).block(); } @Override @@ -165,8 +214,9 @@ public ReactiveTemplateSupport support() { * * @param ex the exception to translate */ - protected RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) { - RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex); + RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) { + RuntimeException resolved = exceptionTranslator != null ? exceptionTranslator.translateExceptionIfPossible(ex) + : null; return resolved == null ? ex : resolved; } @@ -198,4 +248,190 @@ public QueryScanConsistency getConsistency() { return scanConsistency; } + protected Mono doGetDatabase() { + return ReactiveCouchbaseClientUtils.getDatabase(clientFactory, SessionSynchronization.ON_ACTUAL_TRANSACTION); + } + + protected Mono doGetTemplate() { + return ReactiveCouchbaseClientUtils.getTemplate(clientFactory, SessionSynchronization.ON_ACTUAL_TRANSACTION, + this.getConverter()); + } + + /* + private Flux withSession(ReactiveSessionCallback action, ClientSession session) { + + ReactiveSessionBoundCouchbaseTemplate operations = new ReactiveSessionBoundCouchbaseTemplate(session, + ReactiveCouchbaseTemplate.this); + + return Flux.from(action.doInSession(operations)) // + .contextWrite(ctx -> ReactiveMongoContext.setSession(ctx, Mono.just(session))); + } + */ + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(com.mongodb.session.ClientSession) + */ + public ReactiveCouchbaseOperations withSession(ClientSession session) { + return new ReactiveSessionBoundCouchbaseTemplate(session, ReactiveCouchbaseTemplate.this); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(com.mongodb.ClientSessionOptions) + */ + /* + @Override + public ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions) { + return withSession(mongoDatabaseFactory.getSession(sessionOptions)); + } + + */ + + /** + * {@link CouchbaseTemplate} extension bound to a specific {@link ClientSession} that is applied when interacting with + * the server through the driver API.
+ * The prepare steps for {} and {} proxy the target and invoke the desired target method matching the actual arguments + * plus a {@link ClientSession}. + * + * @author Christoph Strobl + * @since 2.1 + */ + static class ReactiveSessionBoundCouchbaseTemplate extends ReactiveCouchbaseTemplate { + + private final ReactiveCouchbaseTemplate delegate; + private final ClientSession session; + + /** + * @param session must not be {@literal null}. + * @param that must not be {@literal null}. + */ + ReactiveSessionBoundCouchbaseTemplate(ClientSession session, ReactiveCouchbaseTemplate that) { + + super(that.clientFactory.withSession(session), that.getConverter()); + + this.delegate = that; + this.session = session; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoTemplate#getCollection(java.lang.String) + */ + @Override + public Collection getCollection(String collectionName) { + + // native MongoDB objects that offer methods with ClientSession must not be proxied. + return delegate.getCollection(collectionName); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoTemplate#getMongoDatabase() + */ + @Override + public ReactiveCouchbaseClientFactory getCouchbaseClientFactory() { + + // native MongoDB objects that offer methods with ClientSession must not be proxied. + return delegate.getCouchbaseClientFactory(); + } + } + + class IndexCreatorEventListener implements ApplicationListener> { + + final Consumer subscriptionExceptionHandler; + + public IndexCreatorEventListener(Consumer subscriptionExceptionHandler) { + this.subscriptionExceptionHandler = subscriptionExceptionHandler; + } + + @Override + public void onApplicationEvent(MappingContextEvent event) { + + if (!event.wasEmittedBy(converter.getMappingContext())) { + return; + } + + // PersistentEntity entity = event.getPersistentEntity(); + + // Double check type as Spring infrastructure does not consider nested generics + // if (entity instanceof MongoPersistentEntity) { + // onCheckForIndexes((MongoPersistentEntity) entity, subscriptionExceptionHandler); + // } + } + } + + /** + * Get the TransactionalOperator from
+ * 1. The template.clientFactory
+ * 2. The template.threadLocal
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() + */ + /* + private CouchbaseStuffHandle getTransactionalOperator() { + if (this.getCouchbaseClientFactory().getTransactionalOperator() != null) { + return this.getCouchbaseClientFactory().getTransactionalOperator(); + } + ReactiveCouchbaseTemplate t = this; + PseudoArgs pArgs = t.getPseudoArgs(); + if (pArgs != null && pArgs.getTxOp() != null) { + return pArgs.getTxOp(); + } + return null; + } + */ + /** + * Value object chaining together a given source document with its mapped representation and the collection to persist + * it to. + * + * @param + * @author Christoph Strobl + * @since 2.2 + */ + /* + private static class PersistableEntityModel { + + private final T source; + private final @Nullable + Document target; + private final String collection; + + private PersistableEntityModel(T source, @Nullable Document target, String collection) { + + this.source = source; + this.target = target; + this.collection = collection; + } + + static PersistableEntityModel of(T source, String collection) { + return new PersistableEntityModel<>(source, null, collection); + } + + static PersistableEntityModel of(T source, Document target, String collection) { + return new PersistableEntityModel<>(source, target, collection); + } + + PersistableEntityModel mutate(T source) { + return new PersistableEntityModel(source, target, collection); + } + + PersistableEntityModel addTargetDocument(Document target) { + return new PersistableEntityModel(source, target, collection); + } + + T getSource() { + return source; + } + + @Nullable + Document getTarget() { + return target; + } + + String getCollection() { + return collection; + } + + */ } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 5a61b1a6a..f7bb2c6ba 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ClientSession; import reactor.core.publisher.Mono; import org.springframework.beans.BeansException; @@ -28,11 +30,9 @@ import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. @@ -69,16 +69,33 @@ ReactiveCouchbaseTemplate getReactiveTemplate() { } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, - String scope, String collection, TransactionResultHolder txResultHolder) { - return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder) { - return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ClientSession session) { + return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, session)); + } + + @Override + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder) { + return applyResult(entity, converted, id, cas, txResultHolder, null); + } + + @Override + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, ClientSession session) { + return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, session)); + } + + @Override + public Integer getTxResultHolder(T source) { + return null; } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index cf7c116ce..2ee4ba153 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -75,7 +75,7 @@ public Mono one(final String id) { LOG.trace("existsById {}", pArgs); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) + .getCollection(pArgs.getCollection()).block().reactive().exists(id, buildOptions(pArgs.getOptions())) .map(ExistsResult::exists)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index ccd5ac529..3d8955edd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -109,7 +109,7 @@ public Mono first() { public Flux all() { return Flux.defer(() -> { String statement = assembleEntityQuery(false); - return template.getCouchbaseClientFactory().getCluster().reactive() + return template.getCouchbaseClientFactory().getCluster().block().reactive() .analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -142,7 +142,7 @@ public Flux all() { public Mono count() { return Mono.defer(() -> { String statement = assembleEntityQuery(true); - return template.getCouchbaseClientFactory().getCluster().reactive() + return template.getCouchbaseClientFactory().getCluster().block().reactive() .analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index 661659baa..f96a70aca 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,7 +31,6 @@ import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; -import com.couchbase.transactions.AttemptContextReactive; /** * Get Operations - method/interface chaining is from the bottom up. @@ -83,7 +83,7 @@ interface FindByIdWithTransaction extends TerminatingFindById, WithTransac * @param txCtx * @return */ - TerminatingFindById transaction(AttemptContextReactive txCtx); + TerminatingFindById transaction(CouchbaseStuffHandle txCtx); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 0fcb1b928..89ad24ea6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -17,6 +17,12 @@ import static com.couchbase.client.java.kv.GetAndTouchOptions.getAndTouchOptions; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionQueryOptions; +import com.example.demo.CouchbaseTransactionalTemplate; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ClientSession; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,7 +35,7 @@ import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.support.PseudoArgs; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.util.Assert; import com.couchbase.client.core.error.DocumentNotFoundException; @@ -38,7 +44,6 @@ import com.couchbase.client.java.codec.RawJsonTranscoder; import com.couchbase.client.java.kv.GetAndTouchOptions; import com.couchbase.client.java.kv.GetOptions; -import com.couchbase.transactions.AttemptContextReactive; public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperation { @@ -62,12 +67,12 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { private final String collection; private final CommonOptions options; private final List fields; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final ReactiveTemplateSupport support; private final Duration expiry; ReactiveFindByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, - CommonOptions options, List fields, Duration expiry, AttemptContextReactive txCtx, + CommonOptions options, List fields, Duration expiry, CouchbaseStuffHandle txCtx, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; @@ -87,20 +92,29 @@ public Mono one(final String id) { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, txCtx, domainType); LOG.trace("findById {}", pArgs); ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive(); - Mono reactiveEntity; - if (pArgs.getCtx() == null) { - if (pArgs.getOptions() instanceof GetAndTouchOptions) { - reactiveEntity = rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()).flatMap( - result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); - } else { - reactiveEntity = rc.get(id, (GetOptions) pArgs.getOptions()).flatMap( - result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType,pArgs.getScope(), pArgs.getCollection(), null)); - } - } else { - reactiveEntity = pArgs.getCtx().get(rc, id).flatMap(result -> support.decodeEntity(id, - result.contentAsObject().toString(), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result))); - } + .getCollection(pArgs.getCollection()).block().reactive(); + + Mono tmpl = template.doGetTemplate(); + //AttemptContextReactive ctx = CouchbaseTransactionalTemplate.getContextReactive(template); + //ClientSession session = CouchbaseTransactionalTemplate.getSession(template); + + Mono reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) + .flatMap(s -> { + if ( s == null || s.getAttemptContextReactive() == null ) { + if (pArgs.getOptions() instanceof GetAndTouchOptions) { + return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()).flatMap( + result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); + } else { + return rc.get(id, (GetOptions) pArgs.getOptions()).flatMap( + result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); + } + } else { + return s.getAttemptContextReactive().get(rc, id) + .flatMap(result -> support.decodeEntity(id, result.contentAsObject().toString(), result.cas(), + domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result), s)); + } + })); + return reactiveEntity.onErrorResume(throwable -> { if (throwable instanceof DocumentNotFoundException) { return Mono.empty(); @@ -159,7 +173,7 @@ public FindByIdWithProjection withExpiry(final Duration expiry) { } @Override - public FindByIdWithProjection transaction(AttemptContextReactive txCtx) { + public FindByIdWithProjection transaction(CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, support); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index c97aa9962..a9db4041a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,7 +33,6 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.transactions.AttemptContextReactive; /** * ReactiveFindByQueryOperation
@@ -152,7 +152,7 @@ interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithT * @return new instance of {@link ReactiveFindByQuery}. * @throws IllegalArgumentException if field is {@literal null}. */ - TerminatingFindByQuery transaction(AttemptContextReactive txCtx); + TerminatingFindByQuery transaction(CouchbaseStuffHandle txCtx); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 341770666..5c53a8a4c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,7 +34,6 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; -import com.couchbase.transactions.AttemptContextReactive; import com.couchbase.transactions.TransactionQueryOptions; /** @@ -70,13 +71,13 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final String[] distinctFields; private final String[] fields; private final QueryOptions options; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, - final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, - final AttemptContextReactive txCtx, final ReactiveTemplateSupport support) { + final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, + final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, + final CouchbaseStuffHandle txCtx, final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); this.template = template; @@ -165,7 +166,7 @@ public FindByQueryWithDistinct distinct(final String[] distinctFields) { } @Override - public FindByQueryWithTransaction transaction(AttemptContextReactive txCtx) { + public FindByQueryWithTransaction transaction(CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null!"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, fields, txCtx, support); @@ -187,16 +188,35 @@ public Flux all() { String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); Mono allResult = null; - CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); - if (pArgs.getCtx() == null) { + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); + /* + if (pArgs.getTxOp() == null) { QueryOptions opts = buildOptions(pArgs.getOptions()); - allResult = pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + allResult = pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - allResult = pArgs.getScope() == null ? pArgs.getCtx().query(statement, opts) : txCtx.query(rs, statement, opts); + allResult = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rs, statement, opts); } + */ + // Mono cluster = template.doGetDatabase(); //doesn't work because there is no interface for Cluster + Mono tmpl = template.doGetTemplate(); + // Mono ctx = TransactionContextManager.currentContext(); + //if (pArgs.getTxOp() == null && txOp == null) { // too early to find TxOp - transactional() has not yet been called + allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) + .flatMap(s -> { + if ( s == null || s.getAttemptContextReactive() == null ) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return s.getAttemptContextReactive() + .query(statement, opts); + } + })); Mono finalAllResult = allResult; return finalAllResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { @@ -242,17 +262,33 @@ public Mono count() { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveScope rc = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); Mono countResult = null; + Mono tmpl = template.doGetTemplate(); + + /* if (txCtx == null) { QueryOptions opts = buildOptions(pArgs.getOptions()); - countResult = pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) - : rc.query(statement, opts); + countResult = pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - countResult = pArgs.getScope() == null ? txCtx.query(statement, opts) : txCtx.query(rc, statement, opts); + countResult = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rc, statement, opts); } + */ + countResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) + .flatMap(s -> { + if ( s == null || s.getAttemptContextReactive() == null ) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return s.getAttemptContextReactive() + .query(statement, opts); + } + })); Mono finalCountResult = countResult; return Mono.defer(() -> finalCountResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index e04237ed7..00760c6c7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -77,8 +77,8 @@ public Mono any(final String id) { LOG.trace("getAnyReplica {}", pArgs); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) + .getCollection(pArgs.getCollection()).block().reactive().getAnyReplica(docId, pArgs.getOptions())) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 7cde94e01..86c53c2d7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,7 +34,6 @@ import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; /** * Insert Operations @@ -104,7 +104,7 @@ interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpir interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { @Override - InsertByIdWithDurability transaction(AttemptContextReactive txCtx); + InsertByIdWithDurability transaction(CouchbaseStuffHandle txCtx); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 728c217ff..8622f75e9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -15,6 +15,13 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.java.Cluster; +import com.example.demo.CouchbaseTransactionalTemplate; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.transaction.reactive.TransactionContext; +import org.springframework.transaction.reactive.TransactionContextManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,7 +34,8 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -35,9 +43,11 @@ import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; import com.couchbase.transactions.TransactionInsertOptions; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; + public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -51,7 +61,7 @@ public ReactiveInsertByIdOperationSupport(final ReactiveCouchbaseTemplate templa public ReactiveInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveInsertByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null, null, template.support()); + DurabilityLevel.NONE, null, (TransactionalOperator) null, template.support()); } static class ReactiveInsertByIdSupport implements ReactiveInsertById { @@ -65,12 +75,13 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; + private final TransactionalOperator txOp; private final ReactiveTemplateSupport support; ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, AttemptContextReactive txCtx, + final DurabilityLevel durabilityLevel, Duration expiry, CouchbaseStuffHandle txCtx, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; @@ -82,28 +93,84 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { this.durabilityLevel = durabilityLevel; this.expiry = expiry; this.txCtx = txCtx; + this.txOp = null; + this.support = support; + } + + ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Duration expiry, TransactionalOperator txOp, + ReactiveTemplateSupport support) { + this.template = template; + this.domainType = domainType; + this.scope = scope; + this.collection = collection; + this.options = options; + this.persistTo = persistTo; + this.replicateTo = replicateTo; + this.durabilityLevel = durabilityLevel; + this.expiry = expiry; + this.txCtx = null; + this.txOp = txOp; this.support = support; } @Override public Mono one(T object) { + // ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager + // .getResource(getRequiredDatabaseFactory()); + + // ((ReactiveCouchbaseResourceHolder) + // TransactionSynchronizationManager.forCurrentTransaction().flatMap((synchronizationManager) -> { + // return Mono.just(synchronizationManager.getResource( template.getCouchbaseClientFactory())); + // }).block()).getSession().getAttemptContextReactive() / + // if (TransactionSynchronizationManager.hasResource(template.getCouchbaseClientFactory())){ + // + // } + // the template should have the session(???) PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); LOG.trace("insertById {}", pArgs); - CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive(); - Mono reactiveEntity; - if (pArgs.getCtx() == null) { - reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> rc - .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + + Mono tmpl = template.doGetTemplate(); + //ClientSession session = CouchbaseTransactionalTemplate.getSession(template); + Mono reactiveEntity = support.encodeEntity(object) + .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { + if (s == null || s.getAttemptContextReactive() == null) { + return template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()) + .flatMap(collection -> collection.reactive() + .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) + .flatMap( + result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + } else { + return s.getAttemptContextReactive() + .insert( + tp.doGetDatabase().block().bucket(tp.getBucketName()).reactive() + .scope(pArgs.getScope() != null ? pArgs.getScope() : DEFAULT_SCOPE) + .collection(pArgs.getCollection() != null ? pArgs.getCollection() : DEFAULT_COLLECTION), + converted.getId(), converted.getContent(), buildTxOptions(pArgs.getOptions(), converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), new TransactionResultHolder(result), s)); + } + }))); + // .flatMap(converted ->/* rc */tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getCluster().flatMap( cl -> + // cl.bucket("my_bucket").reactive() + // .defaultCollection() + // .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) + // .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))))); + /* } else { - reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> pArgs.getCtx() - .insert(rc, converted.getId(), converted.getContent(), buildTxOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), - new TransactionResultHolder(result)))); + reactiveEntity = support.encodeEntity(object).flatMap(converted -> pArgs.getTxOp().getAttemptContextReactive() // transactional() + // needs + // to + // have + // initted + // acr + .insert(template.doGetDatabase().block().bucket("my_bucket").reactive().defaultCollection(), + converted.getId(), converted.getContent(), buildTxOptions(pArgs.getOptions(), converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), + pArgs.getTxOp().transactionResultHolder(result)))); } + */ return reactiveEntity.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { @@ -169,7 +236,7 @@ public InsertByIdWithDurability withExpiry(final Duration expiry) { } @Override - public InsertByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + public InsertByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, support); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index 0cf7c5a48..3f96913bb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -31,7 +32,6 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; /** * Remove Operations on KV service. @@ -103,7 +103,7 @@ interface RemoveByIdWithCas extends RemoveByIdWithDurability { } interface RemoveByIdWithTransaction extends RemoveByIdWithCas, WithTransaction { - RemoveByIdWithCas transaction(AttemptContextReactive txCtx); + RemoveByIdWithCas transaction(CouchbaseStuffHandle txCtx); } interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index a14eac74d..815679dee 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,7 +34,6 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; import com.couchbase.transactions.TransactionGetResult; import com.couchbase.transactions.components.TransactionLinks; @@ -69,15 +69,15 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final TransactionLinks tl = new TransactionLinks(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), false, Optional.empty(), Optional.empty()); ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas, AttemptContextReactive txCtx) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas, CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -95,15 +95,15 @@ public Mono one(final String id) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("removeById {}", pArgs); ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive(); + .getCollection(pArgs.getCollection()).block().reactive(); Mono removeResult; - if (pArgs.getCtx() == null) { + if (pArgs.getTxOp() == null) { removeResult = rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); } else { - Transcoder transcoder = template.getCouchbaseClientFactory().getCluster().environment().transcoder(); + Transcoder transcoder = template.getCouchbaseClientFactory().getCluster().block().environment().transcoder(); TransactionGetResult doc = new TransactionGetResult(id, null, 0, rc, tl, null, Optional.empty(), transcoder, null); - removeResult = pArgs.getCtx().remove(doc).map(r -> new RemoveResult(id, 0, null)); + removeResult = pArgs.getTxOp().getAttemptContextReactive().remove(doc).map(r -> new RemoveResult(id, 0, null)); } return removeResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { @@ -164,7 +164,7 @@ public RemoveByIdWithDurability withCas(Long cas) { } @Override - public RemoveByIdWithCas transaction(AttemptContextReactive txCtx) { + public RemoveByIdWithCas transaction(CouchbaseStuffHandle txCtx) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index b91678672..4f73e5214 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import org.springframework.data.couchbase.core.query.Query; @@ -28,7 +29,6 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.transactions.AttemptContextReactive; /** * RemoveBy Query Operations @@ -92,10 +92,10 @@ interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, W /** * Provide the transaction * - * @param txCtx - transaction - */ + * @param txCtx - transaction + */ @Override - TerminatingRemoveByQuery transaction(AttemptContextReactive txCtx); + TerminatingRemoveByQuery transaction(CouchbaseStuffHandle txCtx); } interface RemoveByQueryTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index f48428eb0..2df3a5aef 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,7 +34,6 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; -import com.couchbase.transactions.AttemptContextReactive; import com.couchbase.transactions.TransactionQueryOptions; public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQueryOperation { @@ -60,11 +61,11 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final String scope; private final String collection; private final QueryOptions options; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, - AttemptContextReactive txCtx) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + CouchbaseStuffHandle txCtx) { this.template = template; this.domainType = domainType; this.query = query; @@ -81,15 +82,15 @@ public Flux all() { String statement = assembleDeleteQuery(pArgs.getCollection()); LOG.trace("removeByQuery {} statement: {}", pArgs, statement); Mono allResult = null; - CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive(); - if (pArgs.getCtx() == null) { + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); + if (pArgs.getTxOp() == null) { QueryOptions opts = buildQueryOptions(pArgs.getOptions()); - allResult = pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts) + allResult = pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(buildQueryOptions(pArgs.getOptions())); - allResult = pArgs.getScope() == null ? txCtx.query(statement, opts) : pArgs.getCtx().query(rs, statement, opts); + allResult = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rs, statement, opts); } Mono finalAllResult = allResult; return Flux.defer(() -> finalAllResult.onErrorMap(throwable -> { @@ -156,10 +157,11 @@ public RemoveByQueryInCollection inScope(final String scope) { } @Override - public RemoveByQueryWithConsistency transaction(final AttemptContextReactive txCtx) { + public RemoveByQueryWithConsistency transaction(final CouchbaseStuffHandle txCtx) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options, txCtx); } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index 25f0b95c5..23843e861 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,7 +34,6 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; /** * ReplaceOperations @@ -50,7 +50,7 @@ public interface ReactiveReplaceByIdOperation { */ ReactiveReplaceById replaceById(Class domainType); - /** + /** * Terminating operations invoking the actual execution. */ interface TerminatingReplaceById extends OneAndAllEntityReactive { @@ -101,7 +101,7 @@ interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExp interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { @Override - ReplaceByIdWithExpiry transaction(final AttemptContextReactive txCtx); + ReplaceByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx); } interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index a363cf697..242cb3f9d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -24,20 +24,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.util.Assert; import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.msg.kv.DurabilityLevel; -import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.AttemptContextReactive; import com.couchbase.transactions.components.TransactionLinks; public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { @@ -67,7 +65,7 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final AttemptContextReactive txCtx; + private final CouchbaseStuffHandle txCtx; private final ReactiveTemplateSupport support; private final TransactionLinks tl = new TransactionLinks(Optional.empty(), Optional.empty(), Optional.empty(), @@ -76,7 +74,7 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final AttemptContextReactive txCtx, + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseStuffHandle txCtx, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; @@ -91,29 +89,62 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { this.support = support; } + /* + @Override + public Mono one(T object) { + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, null, domainType); + LOG.trace("upsertById {}", pArgs); + Mono tmpl = template.doGetTemplate(); + Mono reactiveEntity = support.encodeEntity(object) + .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { + if (s == null || s.getAttemptContextReactive() == null) { + return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) + .flatMap(collection -> collection.reactive() + .replace(converted.getId(), converted.export(), + buildReplaceOptions(pArgs.getOptions(), object, converted)) + .flatMap( + result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + } else { + return Mono.error(new CouchbaseException("No upsert in a transaction. Use insert or replace")); + } + }))); + + return reactiveEntity.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + } + */ @Override public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("replaceById {}", pArgs); - CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive(); + Mono tmpl = template.doGetTemplate(); Mono reactiveEntity; - if (pArgs.getCtx() == null) { - reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> rc - .replace(converted.getId(), converted.export(), - buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); - - } else { - reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> pArgs.getCtx() - .replace(getTransactionHolder(object).transactionGetResult(), - converted.getContent()/*buildTranasactionOptions(pArgs.getOptions(), object, converted)*/) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), - new TransactionResultHolder(result)))); - } + CouchbaseDocument converted = support.encodeEntity(object).block(); + reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { + if (s == null || s.getAttemptContextReactive() == null) { + System.err.println("ReactiveReplaceById: not"); + Mono op = template.getCouchbaseClientFactory() + .withScope(pArgs.getScope()).getCollection(pArgs.getCollection()); + return op.flatMap(collection -> collection.reactive() + .replace(converted.getId(), converted.export(), + buildReplaceOptions(pArgs.getOptions(), object, converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + } else { + System.err.println("ReactiveReplaceById: transaction"); + return s.getAttemptContextReactive() + .replace(s.transactionResultHolder(getTransactionHolder(object)).transactionGetResult(), + converted.getContent()) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), + new TransactionResultHolder(result), s)); + } + })); + return reactiveEntity.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -123,9 +154,12 @@ public Mono one(T object) { }); } - private TransactionResultHolder getTransactionHolder(T object) { - TransactionResultHolder transactionResultHolder; - + private Integer getTransactionHolder(T object) { + Integer transactionResultHolder; + System.err.println("GET: " + System.identityHashCode(object) + " " + object); + if (1 == 1) { + return System.identityHashCode(object); + } transactionResultHolder = template.support().getTxResultHolder(object); if (transactionResultHolder == null) { throw new CouchbaseException( @@ -134,10 +168,6 @@ private TransactionResultHolder getTransactionHolder(T object) { return transactionResultHolder; } - private Object buildTransactionOptions(ReplaceOptions options, T object, CouchbaseDocument doc) { - return OptionsBuilder.buildTransactionReplaceOptions(buildReplaceOptions(options, object, doc)); - } - @Override public Flux all(Collection objects) { return Flux.fromIterable(objects).flatMap(this::one); @@ -190,7 +220,7 @@ public ReplaceByIdWithDurability withExpiry(final Duration expiry) { } @Override - public ReplaceByIdWithExpiry transaction(final AttemptContextReactive txCtx) { + public ReplaceByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, support); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveSessionCallback.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveSessionCallback.java new file mode 100644 index 000000000..c91caa162 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveSessionCallback.java @@ -0,0 +1,45 @@ +package org.springframework.data.couchbase.core; +/* + * Copyright 2018-2021 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. + */ + +import org.reactivestreams.Publisher; + +/** + * Callback interface for executing operations within a {@link com.mongodb.reactivestreams.client.ClientSession} using + * reactive infrastructure. + * + * @author Christoph Strobl + * @since 2.1 + * @see com.mongodb.reactivestreams.client.ClientSession + */ +@FunctionalInterface +public interface ReactiveSessionCallback { + + /** + * Execute operations against a MongoDB instance via session bound {@link ReactiveMongoOperations}. The session is + * inferred directly into the operation so that no further interaction is necessary.
+ * Please note that only Spring Data-specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and + * others are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway + * objects like {@link com.mongodb.reactivestreams.client.MongoCollection} or + * {@link com.mongodb.reactivestreams.client.MongoDatabase} via eg. + * {@link ReactiveMongoOperations#getCollection(String)} we leave responsibility for + * {@link com.mongodb.session.ClientSession} again up to the caller. + * + * @param operations will never be {@literal null}. + * @return never {@literal null}. + */ + Publisher doInSession(ReactiveCouchbaseOperations operations); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index cd0a8e0e8..d4ea03330 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -15,10 +15,11 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.ClientSession; import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; /** @@ -29,14 +30,25 @@ public interface ReactiveTemplateSupport { Mono encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txHolder); + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder); + + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ClientSession session); - Mono applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, TransactionResultHolder txResultHolder); + Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, ClientSession session); + Long getCas(Object entity); String getJavaNameForEntity(Class clazz); - TransactionResultHolder getTxResultHolder(T source); + Integer getTxResultHolder(T source); + + // Integer setTxResultHolder(T source); + + TranslationService getTranslationService(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index ec782fa28..fc112537e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -28,6 +28,7 @@ import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; +import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -81,18 +82,26 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, null, domainType); LOG.trace("upsertById {}", pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + Mono tmpl = template.doGetTemplate(); + Mono reactiveEntity = support.encodeEntity(object) + .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { + if (s == null || s.getAttemptContextReactive() == null) { + return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).flatMap(collection -> collection.reactive() + .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); } else { - return throwable; + return Mono.error(new CouchbaseException("No upsert in a transaction. Use insert or replace")); } - }); + }))); + + return reactiveEntity.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index cad2dfccf..d001c7c99 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -15,9 +15,11 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ClientSession; /** * @@ -29,13 +31,19 @@ public interface TemplateSupport { T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, ClientSession session); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder, ClientSession session); + Long getCas(Object entity); String getJavaNameForEntity(Class clazz); void maybeEmitEvent(CouchbaseMappingEvent event); - TransactionResultHolder getTxResultHolder(T source); + Integer getTxResultHolder(T source); + + TranslationService getTranslationService(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index e7481b91f..e6eec583c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -731,7 +731,6 @@ private CouchbaseList writeCollectionInternal(final Collection source, final target.put(writeCollectionInternal(asCollection(element), new CouchbaseList(conversions.getSimpleTypeHolder()), componentType)); } else { - CouchbaseDocument embeddedDoc = new CouchbaseDocument(); writeInternal(element, embeddedDoc, componentType, false); target.put(embeddedDoc); diff --git a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java index 203efa5eb..e7ce86357 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java @@ -74,7 +74,7 @@ static QueryOptions buildQueryOptions(Query query, QueryOptions options, QuerySc QueryScanConsistency metaQueryScanConsistency = meta.get(SCAN_CONSISTENCY) != null ? ((ScanConsistency) meta.get(SCAN_CONSISTENCY)).query() : null; - QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, getScanConsistency(optsJson), + QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, query.getScanConsistency(), getScanConsistency(optsJson), scanConsistency, metaQueryScanConsistency); Duration timeout = fromFirst(Duration.ofSeconds(0), getTimeout(optsBuilt), meta.get(TIMEOUT)); RetryStrategy retryStrategy = fromFirst(null, getRetryStrategy(optsBuilt), meta.get(RETRY_STRATEGY)); diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index 0de015e5a..88680081c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -23,19 +23,19 @@ import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import com.couchbase.client.core.io.CollectionIdentifier; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; public class PseudoArgs { private final OPTS options; private final String scopeName; private final String collectionName; - private final AttemptContextReactive ctx; + private final CouchbaseStuffHandle transactionalOperator; - public PseudoArgs(String scopeName, String collectionName, OPTS options, AttemptContextReactive ctx) { + public PseudoArgs(String scopeName, String collectionName, OPTS options, CouchbaseStuffHandle transactionalOperator) { this.options = options; this.scopeName = scopeName; this.collectionName = collectionName; - this.ctx = ctx; + this.transactionalOperator = transactionalOperator; } /** @@ -51,12 +51,12 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options, Attempt * @param domainType - entity that may have annotations */ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, - AttemptContextReactive ctx, Class domainType) { + CouchbaseStuffHandle transactionalOperator, Class domainType) { String scopeForQuery = null; String collectionForQuery = null; OPTS optionsForQuery = null; - AttemptContextReactive ctxForQuery = null; + CouchbaseStuffHandle txOpForQuery = null; // 1) repository from DynamicProxy via template threadLocal - has precedence over annotation @@ -66,13 +66,15 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle scopeForQuery = threadLocal.getScope(); collectionForQuery = threadLocal.getCollection(); optionsForQuery = threadLocal.getOptions(); - ctxForQuery = threadLocal.getCtx(); + //throw new RuntimeException("PseudoArgs fix me 1"); + txOpForQuery = threadLocal.getTxOp(); + //System.err.println("threadLocal: txOpForQuery: "+txOpForQuery+" session: "); } scopeForQuery = fromFirst(null, scopeForQuery, scope, getScopeFrom(domainType)); collectionForQuery = fromFirst(null, collectionForQuery, collection, getCollectionFrom(domainType)); optionsForQuery = fromFirst(null, options, optionsForQuery); - ctxForQuery = fromFirst( null, ctx, ctxForQuery); + txOpForQuery = fromFirst( null, transactionalOperator, txOpForQuery /*, template.txOperator() */); // if a collection was specified but no scope, use the scope from the clientFactory @@ -95,7 +97,7 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle throw new CouchbaseException(new IllegalArgumentException("if scope is not default or null, then collection must be specified")); } this.options = optionsForQuery; - this.ctx = ctxForQuery; + this.transactionalOperator = txOpForQuery; } @@ -123,8 +125,8 @@ public String getCollection() { /** * @return the attempt context */ - public AttemptContextReactive getCtx() { - return ctx; + public CouchbaseStuffHandle getTxOp() { + return transactionalOperator; } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java index 77202774e..a4c7825ad 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core.support; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * Interface for operations that take distinct fields @@ -27,7 +27,7 @@ public interface WithTransaction { /** * Specify transactions * - * @param txCtx - */ - Object transaction(AttemptContextReactive txCtx); + * @param txCtx + */ + Object transaction(CouchbaseStuffHandle txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java b/src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java new file mode 100644 index 000000000..4b26edc4a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java @@ -0,0 +1,76 @@ +/* + * Copyright 2021 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.core.transaction; + +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.TransactionsReactive; +import com.couchbase.transactions.config.PerTransactionConfigBuilder; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class SDCouchbaseTransactions { + + Transactions transactions; + Map getResultMap = new HashMap<>(); + private AttemptContextReactive ctx; + + public SDCouchbaseTransactions(Transactions transactions) { + this.transactions = transactions; + } + + public TransactionsReactive reactive(){ + return transactions.reactive(); + } + + public AttemptContextReactive getCtx(){ + return ctx; + } + + // public Mono reactive(Function> transactionLogic) { + // return reactive(transactionLogic, true); + // } + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default PerTransactionConfig. + */ + public Mono reactive(Function> transactionLogic/*, boolean commit*/) { + return transactions.reactive((ctx) -> { + setAttemptTransactionReactive(ctx); + return transactionLogic.apply(ctx); }, PerTransactionConfigBuilder.create().build()/*, commit*/); + + } + + public TransactionResultHolder transactionGetResult(Integer key){ + return getResultMap.get(key); + } + + public TransactionResultHolder transactionGetResult(TransactionGetResult result){ + TransactionResultHolder holder = new TransactionResultHolder(result); + getResultMap.put(System.identityHashCode(holder), holder); + return holder; + } + + public void setAttemptTransactionReactive(AttemptContextReactive ctx) { + this.ctx = ctx; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java index 883864c4d..b4cd5fb98 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -22,7 +22,7 @@ import org.springframework.data.couchbase.repository.support.DynamicInvocationHandler; import com.couchbase.client.java.CommonOptions; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * The generic parameter needs to be REPO which is either a CouchbaseRepository parameterized on T,ID or a @@ -39,14 +39,18 @@ public interface DynamicProxyable { Object getOperations(); /** - * Support for Couchbase-specific options, scope and collections The three "with" methods will return a new proxy - * instance with the specified options, scope, or collections set. The setters are called with the corresponding - * options, scope and collection to set the ThreadLocal fields on the CouchbaseOperations of the repository just - * before the call is made to the repository, and called again with 'null' just after the call is made. The repository - * method will fetch those values to use in the call. + * Support for Couchbase-specific options, scope and collections The four "with" methods will return a new proxy + * instance with the specified options, scope, collection or transactionalOperator set. The setters are called with + * the corresponding options, scope and collection to set the ThreadLocal fields on the CouchbaseOperations of the + * repository just before the call is made to the repository, and called again with 'null' just after the call is + * made. The repository method will fetch those values to use in the call. */ /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param options - the options to set on the returned repository object */ @SuppressWarnings("unchecked") @@ -57,6 +61,10 @@ default REPO withOptions(CommonOptions options) { } /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param scope - the scope to set on the returned repository object */ @SuppressWarnings("unchecked") @@ -67,6 +75,10 @@ default REPO withScope(String scope) { } /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param collection - the collection to set on the returned repository object */ @SuppressWarnings("unchecked") @@ -77,10 +89,18 @@ default REPO withCollection(String collection) { } /** - * @param ctx - the AttemptContextReactive for transactions + * @param ctx - the transactionalOperator for transactions */ @SuppressWarnings("unchecked") - default REPO withTransaction(AttemptContextReactive ctx) { + /* + default REPO withTransaction(TransactionalOperator ctx) { + REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, null, ctx)); + return proxyInstance; + } + */ + + default REPO withTransaction(CouchbaseStuffHandle ctx) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, null, ctx)); return proxyInstance; diff --git a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java index db41820f9..bf8707e99 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java @@ -32,4 +32,5 @@ public interface ReactiveCouchbaseRepository extends ReactiveSortingRepos ReactiveCouchbaseOperations getOperations(); CouchbaseEntityInformation getEntityInformation(); + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/TransactionMeta.java b/src/main/java/org/springframework/data/couchbase/repository/TransactionMeta.java new file mode 100644 index 000000000..ae33ddefa --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/TransactionMeta.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * Indicates the field should hold key to lookup the TransactionGetResult and should NOT be considered part of the + * document. + * + * @author Michael Reiche + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +@Documented +@QueryAnnotation +public @interface TransactionMeta { + + String value() default ""; + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index dda26c076..3ea7bd707 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -51,6 +51,15 @@ public CouchbaseEntityInformation getEntityInformation() { return entityInformation; } + /** + * Returns the repository interface + * + * @return the underlying entity information. + */ + public Class getRepositoryInterface() { + return repositoryInterface; + } + Class getJavaType() { return getEntityInformation().getJavaType(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index 647f1a562..6e64386bb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -29,7 +29,7 @@ import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import com.couchbase.client.java.CommonOptions; -import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * Invocation Handler for scope/collection/options proxy for repositories @@ -46,18 +46,21 @@ public class DynamicInvocationHandler implements InvocationHandler { CommonOptions options; String collection; String scope; - AttemptContextReactive ctx; + CouchbaseStuffHandle ctx; public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope, - AttemptContextReactive ctx) { + CouchbaseStuffHandle ctx) { this.target = target; if (target instanceof CouchbaseRepository) { reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); this.entityInformation = ((CouchbaseRepository) target).getEntityInformation(); } else if (target instanceof ReactiveCouchbaseRepository) { - reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) target).getOperations(); - this.entityInformation = ((ReactiveCouchbaseRepository) target).getEntityInformation(); + reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) this.target).getOperations(); + this.entityInformation = ((ReactiveCouchbaseRepository) this.target).getEntityInformation(); } else { + if( CouchbaseRepository.class.isAssignableFrom(target.getClass())) + System.err.println("isAssignable"); + printInterfaces(target.getClass(), " "); throw new RuntimeException("Unknown target type: " + target.getClass()); } this.options = options; @@ -67,6 +70,15 @@ public DynamicInvocationHandler(T target, CommonOptions options, String colle this.repositoryClass = target.getClass(); } + void printInterfaces(Class clazz, String tab){ + System.out.println(tab+"{"); + for(Class c:clazz.getInterfaces()){ + System.out.println(tab+" " +c.getSimpleName()); + if(c.getInterfaces().length > 0) + printInterfaces(c, tab+" "); + } + System.out.println(tab+"}"); + } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { @@ -94,7 +106,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (method.getName().equals("withTransaction")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, options, collection, scope, (AttemptContextReactive) args[0])); + new DynamicInvocationHandler<>(target, options, collection, scope, (CouchbaseStuffHandle) args[0])); } Class[] paramTypes = null; diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index dc99e37a4..90a485c45 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -26,11 +26,14 @@ import org.reactivestreams.Publisher; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.data.domain.Sort; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; @@ -216,4 +219,26 @@ public ReactiveCouchbaseOperations getOperations() { return operations; } + /** + * Get the TransactionalOperator from
+ * 1. The template.clientFactory
+ * 2. The template.threadLocal
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() + */ + /* + private CouchbaseStuffHandle getTransactionalOperator() { + if (operations.getCouchbaseClientFactory().getTransactionalOperator() != null) { + return operations.getCouchbaseClientFactory().getTransactionalOperator(); + } + ReactiveCouchbaseTemplate t = (ReactiveCouchbaseTemplate) operations; + PseudoArgs pArgs = t.getPseudoArgs(); + if (pArgs != null && pArgs.getTxOp() != null) { + return pArgs.getTxOp(); + } + return null; + } + */ + } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java b/src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java new file mode 100644 index 000000000..7bab2a867 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java @@ -0,0 +1,67 @@ +package org.springframework.data.couchbase.transaction; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.concurrent.Semaphore; + +class AbortCommitSubscriber implements Subscriber { + private Subscription subscription; + private final String name; + private final Semaphore lock; + + public AbortCommitSubscriber(String name){ + this.name = name; + this.lock = new Semaphore(1); + try { + lock.acquire(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + /** + * This method is triggered when the Subscriber subscribes to a Publisher + */ + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } + + /** + * This method is triggered the Subscriber receives an event + * signaling an item being sent from the publisher. The Item is simply printed here. + */ + @Override + public void onNext(T item) { + subscription.request(1); + } + /** + * This method is triggered when the Subscriber receives an error event. + * In our case we just print the error message. + */ + @Override + public void onError(Throwable error) { + System.err.println(name + " Error Occurred: " + error.getMessage()); + } + /** + * This method is triggered when the Subscriber Receives a complete. This means + * it has already received and processed all items from the publisher to which it is subscribed. + */ + @Override + public void onComplete() { + lock.release(); + } + + public Semaphore getLock() { + return lock; + } + + public void waitUntilComplete() { + try { + lock.acquire(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java new file mode 100644 index 000000000..f3f79ac63 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java @@ -0,0 +1,67 @@ +package org.springframework.data.couchbase.transaction; + + +import com.couchbase.client.java.AsyncCluster; +import com.couchbase.client.java.Scope; +import com.couchbase.transactions.AttemptContext; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.transactions.TransactionQueryOptions; +import com.couchbase.transactions.config.TransactionConfig; +import org.reactivestreams.Publisher; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import reactor.core.publisher.Mono; + +import java.util.Map; + +/** + * ClientSession. There is only one implementation - ClientSessionImpl + * The SpringTransaction framework relies on the client session to perform commit() and abort() + * and therefore it has an AttemptContextReactive + * + * @author Michael Reiche + */ +public interface ClientSession /*extends com.mongodb.session.ClientSession*/ { + + Mono getScope(); + + //Mono getScopeReactive(); + + boolean hasActiveTransaction(); + + boolean notifyMessageSent(); + + void notifyOperationInitiated(Object var1); + + //void setAttemptContextReactive(AttemptContextReactive atr); + + AttemptContextReactive getAttemptContextReactive(); + + TransactionOptions getTransactionOptions(); + + AsyncCluster getWrapped(); + + void startTransaction(); + + void startTransaction(TransactionConfig var1); + + Publisher commitTransaction(); + + Publisher abortTransaction(); + + ServerSession getServerSession(); + + void close(); + + Object getClusterTime(); + + Object isCausallyConsistent(); + + T transactionResultHolder(TransactionResultHolder result, T o); + + TransactionResultHolder transactionResultHolder(Integer key); + + AttemptContext getAttemptContext(); + + //ClientSession with(AttemptContextReactive atr); +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java new file mode 100644 index 000000000..b9db58c1e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java @@ -0,0 +1,299 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.transactions.AttemptContext; +import com.couchbase.transactions.AttemptContextReactiveAccessor; +import com.couchbase.transactions.error.external.TransactionOperationFailed; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoSink; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.reactivestreams.Publisher; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.util.Assert; + +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.java.AsyncCluster; +import com.couchbase.client.java.Scope; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionContext; +import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.MergedTransactionConfig; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; +import com.couchbase.transactions.support.AttemptStates; + +public class ClientSessionImpl implements ClientSession { + + protected transient Log logger = LogFactory.getLog(AbstractReactiveTransactionManager.class); + + Mono scopeRx; + Scope scope; + boolean commitInProgress = false; + boolean messageSentInCurrentTransaction = true; // needs to be true for commit + AttemptStates transactionState = AttemptStates.NOT_STARTED; + TransactionOptions transactionOptions; + Transactions transactions; + TransactionContext ctx; + TransactionConfig config; + AttemptContextReactive atr = null; + AttemptContext at = null; + Map getResultMap = new HashMap<>(); + + public ClientSessionImpl(){} + + public ClientSessionImpl(ReactiveCouchbaseClientFactory couchbaseClientFactory, Transactions transactions, + TransactionConfig config, AttemptContextReactive atr) { + this.transactions = transactions; + scopeRx = couchbaseClientFactory.getScope(); + this.config = config == null + ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() + : config; + MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); + ClusterEnvironment environment = couchbaseClientFactory.getCluster().block().environment(); + ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), + now(), Duration.ZERO, merged); + // does this not need an non-reactive AttemptContext? + this.atr = atr; + } + + public ClientSessionImpl(CouchbaseClientFactory couchbaseClientFactory, Transactions transactions, + TransactionConfig config, AttemptContext at) { + this.transactions = transactions; + scope = couchbaseClientFactory.getScope(); + this.config = config == null + ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() + : config; + MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); + ClusterEnvironment environment = couchbaseClientFactory.getCluster().environment(); + ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), + now(), Duration.ZERO, merged); + this.at = at; + if(at != null){ + this.atr = AttemptContextReactiveAccessor.getACR(at); + } + } + + @Override + public Mono getScope() { + return scopeRx; + } + + @Override + public boolean hasActiveTransaction() { + return false; + } + + @Override + public boolean notifyMessageSent() { + return false; + } + + @Override + public void notifyOperationInitiated(Object var1) { + + } + + //@Override + //public void setAttemptContextReactive(AttemptContextReactive atr){ + // this.atr = atr; + //} + + @Override + public AttemptContextReactive getAttemptContextReactive(){ + return atr; + } + + @Override + public AttemptContext getAttemptContext(){ + return at; + } + + + // setter that returns `this` + //@Override + //public ClientSession with(AttemptContextReactive atr){ + // setAttemptContextReactive(atr); + // return this; + //} + + @Override + public TransactionOptions getTransactionOptions() { + return transactionOptions; + } + + @Override + public AsyncCluster getWrapped() { + return null; + } + + @Override + public void startTransaction() { + transactionState = AttemptStates.PENDING; + } + + @Override + public void startTransaction(TransactionConfig var1) { + startTransaction(); + } + + @Override + public Publisher commitTransaction() { + if (this.transactionState == AttemptStates.ABORTED) { + throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction"); + } else if (this.transactionState == AttemptStates.NOT_STARTED) { + throw new IllegalStateException("There is no transaction started"); + } else if (!this.messageSentInCurrentTransaction) { // seems there should have been a messageSent. We just do nothing(?) + this.cleanupTransaction(AttemptStates.COMMITTED); + return Mono.create(MonoSink::success); + } else { + /*ReadConcern readConcern = this.transactionOptions.getReadConcern(); */ + if (0 == 1/* readConcern == null*/) { + throw new CouchbaseException("Invariant violated. Transaction options read concern can not be null"); + } else { + boolean alreadyCommitted = this.commitInProgress || this.transactionState == AttemptStates.COMMITTED; + this.commitInProgress = true; + // this will fail with ctx.serialized() being Optional.empty() + // how does the commit happen in transactions.reactive().run() ? + /* + return transactions.reactive().commit(ctx.serialized().get(), null).then().doOnSuccess(x -> { + commitInProgress = false; + this.transactionState = AttemptStates.COMMITTED; + }).doOnError(CouchbaseException.class, this::clearTransactionContextOnError); + */ + // TODO MSR + // return Mono.create(MonoSink::success); + return executeImplicitCommit(atr).then(); //transactions.reactive().executeImplicitCommit(atr).then(); + /* + return this.executor.execute((new CommitTransactionOperation(this.transactionOptions.getWriteConcern(), alreadyCommitted)).recoveryToken(this.getRecoveryToken()).maxCommitTime(this.transactionOptions.getMaxCommitTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS), readConcern, this).doOnTerminate(() -> { + this.commitInProgress = false; + this.transactionState = AttemptStates.COMMITTED; + }).doOnError(CouchbaseException.class, this::clearTransactionContextOnError); + + */ + } + + } + } + + public Mono executeImplicitCommit(AttemptContextReactive ctx) { + return Mono.defer(() -> { + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to commit ctx %s", ctx)); + } + // If app has not explicitly performed a commit, assume they want to do so anyway + if (0 != 1 /*!ctx.isDone()*/) { + if (0 == 1 /*ctx.serialized().isPresent()*/) { + return Mono.just(ctx); + } else { + //System.err.println(ctx.attemptId()+ " doing implicit commit"); // ctx.LOGGER.trace(); + System.err.println("doing implicit commit"); + if(ctx != null) { + return ctx.commit() + .then(Mono.just(ctx)) + .onErrorResume(err -> Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, + ctx))); + } else { + at.commit(); + return Mono.empty(); + } + } + } else { + System.err.println("Transaction already done"); + System.err.println(ctx.attemptId()+" Transaction already done"); // // ctx.LOGGER.trace(); + return Mono.just(ctx); + } + }); + } + + + + @Override + public Publisher abortTransaction() { + System.err.println("**** abortTransaction ****"); + Assert.notNull(transactions, "transactions"); + Assert.notNull(ctx, "ctx"); + Assert.notNull(ctx.serialized(), "ctx.serialized()"); + if (ctx.serialized().isPresent()) { + Assert.notNull(ctx.serialized().get(), "ctx.serialized().get()"); + return transactions.reactive().rollback(ctx.serialized().get(), null).then(); + } else { + return executeExplicitRollback(atr).then(); + } + } + + private Mono executeExplicitRollback(AttemptContextReactive atr) { + if(at != null){ + at.rollback(); + return Mono.empty(); + } else { + return atr.rollback(); + } + } + + @Override + public ServerSession getServerSession() { + return null; + } + + @Override + public void close() { + + } + + @Override + public Object getClusterTime() { + return null; + } + + @Override + public Object isCausallyConsistent() { + return null; + } + + private void cleanupTransaction(AttemptStates attempState) {} + + private void clearTransactionContext() {} + + private void clearTransactionContextOnError(CouchbaseException e) { + String s = e.getMessage() != null ? e.getMessage().toLowerCase(Locale.ROOT) : null; + if (s != null && (s.contains("transienttransactionerror") || s.contains("unknowntransactioncommitresult"))) { + this.clearTransactionContext(); + } + + } + + @Override + public TransactionResultHolder transactionResultHolder(Integer key) { + TransactionResultHolder holder = getResultMap.get(key); + if(holder == null){ + throw new RuntimeException("did not find transactionResultHolder for key="+key+" in session"); + } + return holder; + } + + @Override + public TransactionResultHolder transactionResultHolder(TransactionResultHolder holder, Object o) { + //TransactionResultHolder holder = new TransactionResultHolder(result); + System.err.println("PUT: "+System.identityHashCode(o)+" "+o); + getResultMap.put(System.identityHashCode(o), holder); + return holder; + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java new file mode 100644 index 000000000..b145f0715 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java @@ -0,0 +1,115 @@ +package org.springframework.data.couchbase.transaction; + +import java.util.Objects; + +import org.springframework.data.annotation.Immutable; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import com.couchbase.transactions.TransactionQueryOptions; + +@Immutable +public final class ClientSessionOptions { + private final Boolean causallyConsistent; + private final Boolean snapshot; + private final TransactionQueryOptions defaultTransactionOptions; + + @Nullable + public Boolean isCausallyConsistent() { + return this.causallyConsistent; + } + + @Nullable + public Boolean isSnapshot() { + return this.snapshot; + } + + public TransactionQueryOptions getDefaultTransactionOptions() { + return this.defaultTransactionOptions; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o != null && this.getClass() == o.getClass()) { + ClientSessionOptions that = (ClientSessionOptions) o; + if (!Objects.equals(this.causallyConsistent, that.causallyConsistent)) { + return false; + } else if (!Objects.equals(this.snapshot, that.snapshot)) { + return false; + } else { + return Objects.equals(this.defaultTransactionOptions, that.defaultTransactionOptions); + } + } else { + return false; + } + } + + public int hashCode() { + int result = this.causallyConsistent != null ? this.causallyConsistent.hashCode() : 0; + result = 31 * result + (this.snapshot != null ? this.snapshot.hashCode() : 0); + result = 31 * result + (this.defaultTransactionOptions != null ? this.defaultTransactionOptions.hashCode() : 0); + return result; + } + + public String toString() { + return "ClientSessionOptions{causallyConsistent=" + this.causallyConsistent + "snapshot=" + this.snapshot + + ", defaultTransactionOptions=" + this.defaultTransactionOptions + '}'; + } + + public static ClientSessionOptions.Builder builder() { + return new ClientSessionOptions.Builder(); + } + + public static ClientSessionOptions.Builder builder(ClientSessionOptions options) { + Assert.notNull(options, "options"); + ClientSessionOptions.Builder builder = new ClientSessionOptions.Builder(); + builder.causallyConsistent = options.isCausallyConsistent(); + builder.snapshot = options.isSnapshot(); + builder.defaultTransactionOptions = options.getDefaultTransactionOptions(); + return builder; + } + + private ClientSessionOptions(ClientSessionOptions.Builder builder) { + if (builder.causallyConsistent != null && builder.causallyConsistent && builder.snapshot != null + && builder.snapshot) { + throw new IllegalArgumentException("A session can not be both a snapshot and causally consistent"); + } else { + this.causallyConsistent = builder.causallyConsistent == null && builder.snapshot != null ? !builder.snapshot + : builder.causallyConsistent; + this.snapshot = builder.snapshot; + this.defaultTransactionOptions = builder.defaultTransactionOptions; + } + } + + // @NotThreadSafe + public static final class Builder { + private Boolean causallyConsistent; + private Boolean snapshot; + private TransactionQueryOptions defaultTransactionOptions; + + public ClientSessionOptions.Builder causallyConsistent(boolean causallyConsistent) { + this.causallyConsistent = causallyConsistent; + return this; + } + + public ClientSessionOptions.Builder snapshot(boolean snapshot) { + this.snapshot = snapshot; + return this; + } + + public ClientSessionOptions.Builder defaultTransactionOptions(TransactionQueryOptions defaultTransactionOptions) { + Assert.notNull(defaultTransactionOptions, "defaultTransactionOptions"); + this.defaultTransactionOptions = defaultTransactionOptions; + return this; + } + + public ClientSessionOptions build() { + return new ClientSessionOptions(this); + } + + private Builder() { + /* TODO this.defaultTransactionOptions = TransactionQueryOptions.builder().build();*/ + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java new file mode 100644 index 000000000..9f5b3f7f5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 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.transaction; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.transaction.reactive.TransactionalOperator; + +import com.couchbase.client.core.error.CouchbaseException; +// import com.couchbase.transactions.AttemptContextReactive; + + +/** + * This is a proxy for AttemptContextReactive that also has the transactionalOperator, so that it can provide the + * transactionalOperator to the repository and templates used within the transaction lambda via ctx.template(templ) and + * ctx.repository(repo) + */ +public interface CouchbaseAttemptContextReactive { + + > R repository(R repo); + + ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template); + + static CouchbaseAttemptContextReactive proxyFor(/*AttemptContextReactive acr,*/ TransactionalOperator txOperator) { + Class[] interfaces = new Class[] { /* AttemptContextReactiveInterface.class, */ + CouchbaseAttemptContextReactive.class }; + CouchbaseAttemptContextReactive proxyInstance = (CouchbaseAttemptContextReactive) Proxy.newProxyInstance( + txOperator.getClass().getClassLoader(), interfaces, + new CouchbaseAttemptContextReactive.ACRInvocationHandler(/*acr,*/ txOperator)); + return proxyInstance; + } + + class ACRInvocationHandler implements InvocationHandler { + + // final AttemptContextReactive acr; + final TransactionalOperator txOperator; + + public ACRInvocationHandler(/*AttemptContextReactive acr,*/ TransactionalOperator txOperator) { +// this.acr = acr; + this.txOperator = txOperator; + } + + public ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template) { + ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) txOperator) + .getTransactionManager()); + if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Template must use the same clientFactory as the transactionManager of the transactionalOperator " + + template); + } + return template;//.with((CouchbaseStuffHandle) txOperator); // this returns a new template with a new + // couchbaseClient with txOperator + } + + public > R repository(R repo) { + if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) { + throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo); + } + ReactiveCouchbaseOperations reactiveOperations = (ReactiveCouchbaseOperations) repo.getOperations(); + ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) txOperator) + .getTransactionManager()); + + if (reactiveOperations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + + repo); + } + return repo.withTransaction((CouchbaseStuffHandle) txOperator); // this returns a new repository proxy with txOperator in its threadLocal + // what if instead we returned a new repo with a new template with the txOperator? + } + + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + if (method.getName().equals("template")) { + return template((ReactiveCouchbaseTemplate) objects[0]); + } + if (method.getName().equals("repository")) { + return repository((DynamicProxyable) objects[0]); + } + throw new UnsupportedOperationException(method.toString()); + //return method.invoke(acr, objects); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java new file mode 100644 index 000000000..27bbd7c94 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java @@ -0,0 +1,294 @@ +/* + * Copyright 2021 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.transaction; + +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; + +import com.couchbase.client.java.Cluster; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.AttemptContextReactiveAccessor; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.error.external.TransactionOperationFailed; +import reactor.util.context.ContextView; + +/** + * Blocking TransactionManager + * + * @author Michael Nitschinger + * @author Michael Reiche + */ + +public class CouchbaseCallbackTransactionManager extends AbstractPlatformTransactionManager + implements DisposableBean, ResourceTransactionManager, CallbackPreferringPlatformTransactionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); + + private final CouchbaseTemplate template; + private final ReactiveCouchbaseTemplate reactiveTemplate; + private Transactions transactions; + private final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; + private final CouchbaseClientFactory couchbaseClientFactory; + + private ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction; + + public CouchbaseCallbackTransactionManager(CouchbaseTemplate template, ReactiveCouchbaseTemplate reactiveTemplate, + TransactionConfig transactionConfig) { + this.template = template; + this.reactiveTemplate = reactiveTemplate; + this.transactions = Transactions.create((Cluster) (template().getCouchbaseClientFactory().getCluster().block()), + transactionConfig); + this.reactiveCouchbaseClientFactory = this.reactiveTemplate.getCouchbaseClientFactory(); + this.couchbaseClientFactory = this.template.getCouchbaseClientFactory(); + } + + public ReactiveCouchbaseTemplate template() { + return reactiveTemplate; + } + + private CouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options, + AttemptContextReactive atr) { + + CouchbaseClientFactory databaseFactory = template.getCouchbaseClientFactory(); + + CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder( + databaseFactory.getSession(options, transactions, null, atr), databaseFactory); + return resourceHolder; + } + + @Override + public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { + final AtomicReference execResult = new AtomicReference<>(); + AtomicReference startTime = new AtomicReference<>(0L); + + Mono txnResult = transactions.reactive().run(ctx -> { + /* begin spring-data-couchbase transaction 1/2 */ + ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder reactiveResourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + + CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(clientSession, + template.getCouchbaseClientFactory()); + + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new) + . flatMap(synchronizationManager -> { + System.err.println("CallbackTransactionManager: " + this); + System.err.println("bindResource: " + reactiveCouchbaseClientFactory.getCluster().block()); + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), + reactiveResourceHolder); + org.springframework.transaction.support.TransactionSynchronizationManager + .unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); + org.springframework.transaction.support.TransactionSynchronizationManager + .bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction = new ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject( + reactiveResourceHolder); + setTransaction(transaction); + + /* end spring-data-couchbase transaction 1/2 */ + + Mono result = TransactionSynchronizationManager.forCurrentTransaction().flatMap((sm) -> { + sm.unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); + sm.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), + reactiveResourceHolder); + CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(transaction, true, false, false, true, null, sm); + prepareSynchronization(status, new CouchbaseTransactionDefinition()); + // System.err.println("deferContextual.ctx : " + xxx); + //Mono cxView = Mono.deferContextual(cx -> { System.err.println("CallbackTransactionManager.cx: "+cx); return Mono.just(cx);}); + try { + // Since we are on a different thread now transparently, at least make sure + // that the original method invocation is synchronized. + synchronized (this) { + execResult.set(callback.doInTransaction(status)); + } + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + return Mono.empty(); + }).contextWrite(TransactionContextManager.getOrCreateContext()) // this doesn't create a context on the desired publisher + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); + + result.onErrorResume(err -> { + AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), + "caught exception '%s' in async, rethrowing", err); + return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); + }).thenReturn(ctx); + return result.then(Mono.just(synchronizationManager)); + }); + /* begin spring-data-couchbase transaction 2/2 */ // this doesn't create a context on the desired publisher + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); + /* end spring-data-couchbase transaction 2/2 */ + }).doOnSubscribe(v -> startTime.set(System.nanoTime())); + + txnResult.block(); + return execResult.get(); // transactions.reactive().executeTransaction(merged,overall,ob).doOnNext(v->overall.span().finish()).doOnError(err->overall.span().failWith(err));}); + + } + + private void setTransaction(ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction) { + this.transactions = transactions; + } + + @Override + protected ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject doGetTransaction() + throws TransactionException { + /* + CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + return new CouchbaseTransactionManager.CouchbaseTransactionObject(resourceHolder); + */ + return (ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject) transaction; + } + + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractTransaction(transaction).hasResourceHolder(); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + LOGGER.debug("Beginning Couchbase Transaction with Definition {}", definition); + } + + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + LOGGER.debug("Committing Couchbase Transaction with status {}", status); + } + + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + LOGGER.warn("Rolling back Couchbase Transaction with status {}", status); + org.springframework.transaction.support.TransactionSynchronizationManager + .unbindResource(reactiveCouchbaseClientFactory); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + LOGGER.trace("Performing cleanup of Couchbase Transaction {}", transaction); + org.springframework.transaction.support.TransactionSynchronizationManager + .unbindResource(reactiveCouchbaseClientFactory); + return; + } + + @Override + public void destroy() { + transactions.close(); + } + + @Override + public Object getResourceFactory() { + return reactiveTemplate.getCouchbaseClientFactory(); + } + + private static CouchbaseTransactionObject extractTransaction(Object transaction) { + Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + transaction.getClass())); + + return (CouchbaseTransactionObject) transaction; + } + /* + public class CouchbaseResourceHolder extends ResourceHolderSupport { + + private volatile AttemptContextReactive attemptContext; + //private volatile TransactionResultMap resultMap = new TransactionResultMap(template); + + public CouchbaseResourceHolder(AttemptContextReactive attemptContext) { + this.attemptContext = attemptContext; + } + + public AttemptContextReactive getAttemptContext() { + return attemptContext; + } + + public void setAttemptContext(AttemptContextReactive attemptContext) { + this.attemptContext = attemptContext; + } + + //public TransactionResultMap getTxResultMap() { + // return resultMap; + //} + + @Override + public String toString() { + return "CouchbaseResourceHolder{" + "attemptContext=" + attemptContext + "}"; + } + } + + */ + + protected static class CouchbaseTransactionObject implements SmartTransactionObject { + + private final CouchbaseResourceHolder resourceHolder; + + CouchbaseTransactionObject(CouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + @Override + public boolean isRollbackOnly() { + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); + } + + @Override + public void flush() { + TransactionSynchronizationUtils.triggerFlush(); + } + + public boolean hasResourceHolder() { + return resourceHolder != null; + } + + @Override + public String toString() { + return "CouchbaseTransactionObject{" + "resourceHolder=" + resourceHolder + '}'; + } + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java new file mode 100644 index 000000000..43a4b159c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java @@ -0,0 +1,157 @@ +/* + * Copyright 2019-2021 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.transaction; + +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; + +/** + * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveCouchbaseTransactionManager} binds + * instances of this class to the subscriber context. + *

+ * Note: Intended for internal usage only. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.2 + * @see CouchbaseTransactionManager + * @see CouchbaseTemplate + */ + +public class CouchbaseResourceHolder extends ResourceHolderSupport { + + private @Nullable ClientSession session; // which holds the atr + private CouchbaseClientFactory databaseFactory; + + /** + * Create a new {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} for a given + * {@link ClientSession session}. + * + * @param session the associated {@link ClientSession}. Can be {@literal null}. + * @param databaseFactory the associated {@link CouchbaseClientFactory}. must not be {@literal null}. + */ + public CouchbaseResourceHolder(@Nullable ClientSession session, CouchbaseClientFactory databaseFactory) { + + this.session = session; + this.databaseFactory = databaseFactory; + } + + /** + * @return the associated {@link ClientSession}. Can be {@literal null}. + */ + @Nullable + public ClientSession getSession() { + return session; + } + + /** + * @return the required associated {@link ClientSession}. + * @throws IllegalStateException if no session is associated. + */ + ClientSession getRequiredSession() { + + ClientSession session = getSession(); + + if (session == null) { + throw new IllegalStateException("No ClientSession associated"); + } + return session; + } + + /** + * @return the associated {@link CouchbaseClientFactory}. + */ + public CouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + /** + * Set the {@link ClientSession} to guard. + * + * @param session can be {@literal null}. + */ + public void setSession(@Nullable ClientSession session) { + this.session = session; + } + + /** + * @return {@literal true} if session is not {@literal null}. + */ + boolean hasSession() { + return session != null; + } + + /** + * If the {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} is + * {@link #hasSession() not already associated} with a {@link ClientSession} the given value is + * {@link #setSession(ClientSession) set} and returned, otherwise the current bound session is returned. + * + * @param session + * @return + */ + @Nullable + public ClientSession setSessionIfAbsent(@Nullable ClientSession session) { + + if (!hasSession()) { + setSession(session); + } + + return session; + } + + /** + * @return {@literal true} if the session is active and has not been closed. + */ + boolean hasActiveSession() { + + if (!hasSession()) { + return false; + } + + return hasServerSession() && !getRequiredSession().getServerSession().isClosed(); + } + + /** + * @return {@literal true} if the session has an active transaction. + * @see #hasActiveSession() + */ + boolean hasActiveTransaction() { + + if (!hasActiveSession()) { + return false; + } + + return getRequiredSession().hasActiveTransaction(); + } + + /** + * @return {@literal true} if the {@link ClientSession} has a {link com.mongodb.session.ServerSession} associated that + * is accessible via {@link ClientSession#getServerSession()}. + */ + boolean hasServerSession() { + + try { + return getRequiredSession().getServerSession() != null; + } catch (IllegalStateException serverSessionClosed) { + // ignore + } + + return false; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java new file mode 100644 index 000000000..aeccf9f4e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java @@ -0,0 +1,144 @@ +package org.springframework.data.couchbase.transaction; + +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.util.Assert; + +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.TransactionsReactive; + +public class CouchbaseStuffHandle { + + // package org.springframework.transaction.reactive; + private static final Log logger = LogFactory.getLog(CouchbaseStuffHandle.class); + private final ReactiveTransactionManager transactionManager; + private final TransactionDefinition transactionDefinition; + + Map getResultMap = new HashMap<>(); + private AttemptContextReactive attemptContextReactive; + + public CouchbaseStuffHandle() { + transactionManager = null; + transactionDefinition = null; + } + + public CouchbaseStuffHandle(ReactiveCouchbaseTransactionManager transactionManager) { + this(transactionManager, new CouchbaseTransactionDefinition()); + } + + public CouchbaseStuffHandle(ReactiveCouchbaseTransactionManager transactionManager, + TransactionDefinition transactionDefinition) { + Assert.notNull(transactionManager, "ReactiveTransactionManager must not be null"); + Assert.notNull(transactionDefinition, "TransactionDefinition must not be null"); + this.transactionManager = transactionManager; + this.transactionDefinition = transactionDefinition; + } + + public Mono reactive(Function> transactionLogic) { + return reactive(transactionLogic, true); + } + + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default + * PerTransactionConfig. + */ + public Mono reactive(Function> transactionLogic, + boolean commit) { + return ((ReactiveCouchbaseTransactionManager) transactionManager).getTransactions().reactive((ctx) -> { + setAttemptContextReactive(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport + // for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method + // and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder() + return transactionLogic.apply(this); + }/*, commit*/); + } + + public TransactionResultHolder transactionResultHolder(Integer key) { + return getResultMap.get(key); + } + + public TransactionResultHolder transactionResultHolder(TransactionGetResult result) { + TransactionResultHolder holder = new TransactionResultHolder(result); + getResultMap.put(System.identityHashCode(holder), holder); + return holder; + } + + public void setAttemptContextReactive(AttemptContextReactive attemptContextReactive) { + this.attemptContextReactive = attemptContextReactive; + // see ReactiveCouchbaseTransactionManager.doBegin() + // transactionManager.getReactiveTransaction(new CouchbaseTransactionDefinition()).block(); + // CouchbaseResourceHolder holder = null; + //TransactionSynchronizationManager.bindResource(((ReactiveCouchbaseTransactionManager)transactionManager).getDatabaseFactory(), holder); + + /* + for savePerson that, doBegin() is called from AbstractReactiveTransactionManager.getReactiveTransaction() + which is called from TransactionalOperatorImpl.transactional(Mono) + [also called from TransactionalOperatorImpl.execute(TransactionCallback)] + */ + } + + public AttemptContextReactive getAttemptContextReactive() { + return attemptContextReactive; + } + + public ReactiveTransactionManager getTransactionManager() { + return transactionManager; + } + + public ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template) { + ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) this) + .getTransactionManager()); + if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Template must use the same clientFactory as the transactionManager of the transactionalOperator " + + template); + } + return template;// .with(this); // template with a new couchbaseClient with txOperator + } + + /* + public CouchbaseTemplate template(CouchbaseTemplate template) { + CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) ((CouchbaseStuffHandle) this) + .getTransactionManager()); + if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Template must use the same clientFactory as the transactionManager of the transactionalOperator " + + template); + } + return template.with(this); // template with a new couchbaseClient with txOperator + } +*/ + public > R repository(R repo) { + if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) { + throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo); + } + ReactiveCouchbaseOperations reactiveOperations = (ReactiveCouchbaseOperations) repo.getOperations(); + ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) this) + .getTransactionManager()); + + if (reactiveOperations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + repo); + } + return repo.withTransaction(this); // this returns a new repository proxy with txOperator in its threadLocal + // what if instead we returned a new repo with a new template with the txOperator? + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java new file mode 100644 index 000000000..006df028f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java @@ -0,0 +1,36 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.transactions.AttemptContext; +import com.couchbase.transactions.AttemptContextReactive; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +public class CouchbaseTransactionDefinition extends DefaultTransactionDefinition { + + AttemptContextReactive atr; + AttemptContext at; + + public CouchbaseTransactionDefinition(){ + super(); + } + + public CouchbaseTransactionDefinition(TransactionDefinition that) { + super(that); + } + + public CouchbaseTransactionDefinition(int propagationBehavior) { + super(propagationBehavior); + } + + public void setAttemptContextReactive(AttemptContextReactive atr){ + this.atr = atr; + } + + public AttemptContextReactive getAttemptContextReactive(){ + return atr; + } + + public void setAttemptContext(AttemptContext attemptContext) { + at = attemptContext; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java new file mode 100644 index 000000000..840903624 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java @@ -0,0 +1,544 @@ +/* + * Copyright 2018-2021 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.transaction; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; +import reactor.core.publisher.Mono; + +/** + * A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages + * {@link ClientSession} based transactions for a single {@link CouchbaseClientFactory}. + *

+ * Binds a {@link ClientSession} from the specified {@link CouchbaseClientFactory} to the thread. + *

+ * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal + * consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction() + * commit} or {@link ClientSession#abortTransaction() abort} a transaction. + *

+ * TODO: Application code is required to retrieve the {@link com.couchbase.client.java.Cluster} ????? via + * {@link ?????#getDatabase(CouchbaseClientFactory)} instead of a standard {@link CouchbaseClientFactory#getCluster()} + * call. Spring classes such as {@link org.springframework.data.couchbase.core.CouchbaseTemplate} use this strategy + * implicitly. + *

+ * By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. One may override + * {@link #doCommit(CouchbaseTransactionObject)} to implement the + * Retry Commit Operation + * behavior as outlined in the MongoDB reference manual. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + * @see MongoDB Transaction Documentation + * @see MongoDatabaseUtils#getDatabase(CouchbaseClientFactory, SessionSynchronization) + */ +public class CouchbaseTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + private @Nullable CouchbaseClientFactory databaseFactory; + private @Nullable Transactions transactions; // This is the com.couchbase.transactions object + private @Nullable TransactionConfig config; + private @Nullable TransactionOptions options; + + /** + * Create a new {@link CouchbaseTransactionManager} for bean-style usage. + *

+ * Note:The {@link CouchbaseClientFactory db factory} has to be + * {@link #setDbFactory(CouchbaseClientFactory) set} before using the instance. Use this constructor to prepare a + * {@link CouchbaseTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}. + *

+ * TODO: Optionally it is possible to set default {@link TransactionOptions transaction options} defining TODO: + * {@link ReadConcern} and {@link WriteConcern}. + * + * @see #setDbFactory(CouchbaseClientFactory) + * @see #setTransactionSynchronization(int) + */ + public CouchbaseTransactionManager() {} + + /** + * Create a new {@link CouchbaseTransactionManager} obtaining sessions from the given {@link CouchbaseClientFactory}. + * + * @param databaseFactory must not be {@literal null}. + */ + public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory) { + this(databaseFactory, null); + } + + /** + * Create a new {@link CouchbaseTransactionManager} obtaining sessions from the given {@link CouchbaseClientFactory} + * applying the given {@link TransactionOptions options}, if present, when starting a new transaction. + * + * @param databaseFactory must not be {@literal null}. @//param options can be {@literal null}. + */ + public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory, @Nullable Transactions transactions) { + + Assert.notNull(databaseFactory, "DbFactory must not be null!"); + System.err.println(this); + System.err.println(databaseFactory.getCluster()); + this.databaseFactory = databaseFactory; + this.transactions = transactions; + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction() + */ + @Override + protected Object doGetTransaction() throws TransactionException { + + CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(getRequiredDatabaseFactory().getCluster()); + return new CouchbaseTransactionObject(resourceHolder); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#isExistingTransaction(java.lang.Object) + */ + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractCouchbaseTransaction(transaction).hasResourceHolder(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doBegin(java.lang.Object, org.springframework.transaction.TransactionDefinition) + */ + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); +// should ACR already be in TSM? TransactionSynchronizationManager.bindResource(getRequiredDbFactory().getCluster(), resourceHolder); + CouchbaseResourceHolder resourceHolder = newResourceHolder(definition, + ClientSessionOptions.builder().causallyConsistent(true).build(), + null /* ((CouchbaseTransactionDefinition) definition).getAttemptContextReactive()*/); + couchbaseTransactionObject.setResourceHolder(resourceHolder); + + if (logger.isDebugEnabled()) { + logger + .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession()))); + } + + try { + couchbaseTransactionObject.startTransaction(options); + } catch (CouchbaseException ex) { + throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.", + debugString(couchbaseTransactionObject.getSession())), ex); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession()))); + } + + resourceHolder.setSynchronizedWithTransaction(true); + System.err.println("CouchbaseTransactionManager: "+this); + System.err.println("bindResource: "+ getRequiredDatabaseFactory().getCluster()+" value: "+resourceHolder); + TransactionSynchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster(), resourceHolder); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doSuspend(java.lang.Object) + */ + @Override + protected Object doSuspend(Object transaction) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); + couchbaseTransactionObject.setResourceHolder(null); + + return TransactionSynchronizationManager.unbindResource(getRequiredDatabaseFactory().getCluster()); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doResume(java.lang.Object, java.lang.Object) + */ + @Override + protected void doResume(@Nullable Object transaction, Object suspendedResources) { + TransactionSynchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster(), suspendedResources); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected final void doCommit(DefaultTransactionStatus status) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to commit transaction for session %s.", + debugString(couchbaseTransactionObject.getSession()))); + } + + try { + doCommit(couchbaseTransactionObject); + } catch (Exception ex) { + logger.debug("could not commit Couchbase transaction for session "+debugString(couchbaseTransactionObject.getSession())); + throw new TransactionSystemException(String.format("Could not commit Couchbase transaction for session %s.", + debugString(couchbaseTransactionObject.getSession())), ex); + } + } + + /** + * Customization hook to perform an actual commit of the given transaction.
+ * If a commit operation encounters an error, the MongoDB driver throws a {@link CouchbaseException} holding + * {@literal error labels}.
+ * By default those labels are ignored, nevertheless one might check for + * {@link CouchbaseException#UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL transient commit errors labels} and retry the the + * commit.
+ * + *

+	 * int retries = 3;
+	 * do {
+	 *     try {
+	 *         transactionObject.commitTransaction();
+	 *         break;
+	 *     } catch (CouchbaseException ex) {
+	 *         if (!ex.hasErrorLabel(CouchbaseException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
+	 *             throw ex;
+	 *         }
+	 *     }
+	 *     Thread.sleep(500);
+	 * } while (--retries > 0);
+	 *     
+ * + * + * @param transactionObject never {@literal null}. + * @throws Exception in case of transaction errors. + */ + protected void doCommit(CouchbaseTransactionObject transactionObject) throws Exception { + transactionObject.commitTransaction(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to abort transaction for session %s.", + debugString(couchbaseTransactionObject.getSession()))); + } + + try { + couchbaseTransactionObject.abortTransaction(); + } catch (CouchbaseException ex) { + + throw new TransactionSystemException(String.format("Could not abort Couchbase transaction for session %s.", + debugString(couchbaseTransactionObject.getSession())), ex); + } + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doSetRollbackOnly(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { + + CouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status); + transactionObject.getRequiredResourceHolder().setRollbackOnly(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object) + */ + @Override + protected void doCleanupAfterCompletion(Object transaction) { + + Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + transaction.getClass())); + + CouchbaseTransactionObject couchbaseTransactionObject = (CouchbaseTransactionObject) transaction; + + // Remove the connection holder from the thread. + TransactionSynchronizationManager.unbindResource(getRequiredDatabaseFactory().getCluster()); + couchbaseTransactionObject.getRequiredResourceHolder().clear(); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to release Session %s after transaction.", + debugString(couchbaseTransactionObject.getSession()))); + } + + couchbaseTransactionObject.closeSession(); + } + + /** + * Set the {@link CouchbaseClientFactory} that this instance should manage transactions for. + * + * @param databaseFactory must not be {@literal null}. + */ + public void setDbFactory(CouchbaseClientFactory databaseFactory) { + + Assert.notNull(databaseFactory, "DbFactory must not be null!"); + this.databaseFactory = databaseFactory; + } + + /** + * Set the {@link TransactionOptions} to be applied when starting transactions. + * + * @param options can be {@literal null}. + */ + public void setOptions(@Nullable TransactionOptions options) { + this.options = options; + } + + /** + * Get the {@link CouchbaseClientFactory} that this instance manages transactions for. + * + * @return can be {@literal null}. + */ + @Nullable + public CouchbaseClientFactory getDbFactory() { + return databaseFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceTransactionManager#getResourceFactory() + */ + @Override + public CouchbaseClientFactory getResourceFactory() { + return getRequiredDatabaseFactory(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + getRequiredDatabaseFactory(); + } + + private CouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options, + AttemptContextReactive atr) { + + CouchbaseClientFactory databaseFactory = getResourceFactory(); + + CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder( + databaseFactory.getSession(options, transactions, null, atr), databaseFactory); + // TODO resourceHolder.setTimeoutIfNotDefaulted(determineTimeout(definition)); + + return resourceHolder; + } + + /** + * @throws IllegalStateException if {@link #databaseFactory} is {@literal null}. + */ + private CouchbaseClientFactory getRequiredDatabaseFactory() { + + Assert.state(databaseFactory != null, + "MongoTransactionManager operates upon a MongoDbFactory. Did you forget to provide one? It's required."); + + return databaseFactory; + } + + private static CouchbaseTransactionObject extractCouchbaseTransaction(Object transaction) { + + Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + transaction.getClass())); + + return (CouchbaseTransactionObject) transaction; + } + + private static CouchbaseTransactionObject extractCouchbaseTransaction(DefaultTransactionStatus status) { + + Assert.isInstanceOf(CouchbaseTransactionObject.class, status.getTransaction(), + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + status.getTransaction().getClass())); + + return (CouchbaseTransactionObject) status.getTransaction(); + } + + private static String debugString(@Nullable ClientSession session) { + + if (session == null) { + return "null"; + } + + String debugString = String.format("[%s@%s ", ClassUtils.getShortName(session.getClass()), + Integer.toHexString(session.hashCode())); + + try { + if (session.getServerSession() != null) { + debugString += String.format("id = %s, ", session.getServerSession().getIdentifier()); + debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent()); + debugString += String.format("txActive = %s, ", session.hasActiveTransaction()); + debugString += String.format("txNumber = %d, ", session.getServerSession().getTransactionNumber()); + debugString += String.format("closed = %d, ", session.getServerSession().isClosed()); + debugString += String.format("clusterTime = %s", session.getClusterTime()); + } else { + debugString += "id = n/a"; + debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent()); + debugString += String.format("txActive = %s, ", session.hasActiveTransaction()); + debugString += String.format("clusterTime = %s", session.getClusterTime()); + } + } catch (RuntimeException e) { + debugString += String.format("error = %s", e.getMessage()); + } + + debugString += "]"; + + return debugString; + } + + public CouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + public Transactions getTransactions() { + return transactions; + } + + /** + * MongoDB specific transaction object, representing a {@link CouchbaseResourceHolder}. Used as transaction object by + * {@link CouchbaseTransactionManager}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + * @see CouchbaseResourceHolder + */ + protected static class CouchbaseTransactionObject implements SmartTransactionObject { + + private @Nullable CouchbaseResourceHolder resourceHolder; + + CouchbaseTransactionObject(@Nullable CouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * Set the {@link CouchbaseResourceHolder}. + * + * @param resourceHolder can be {@literal null}. + */ + void setResourceHolder(@Nullable CouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * @return {@literal true} if a {@link CouchbaseResourceHolder} is set. + */ + final boolean hasResourceHolder() { + return resourceHolder != null; + } + + /** + * Start a MongoDB transaction optionally given {@link TransactionOptions}. + * + * @param options can be {@literal null} + */ + void startTransaction(TransactionOptions options) { + + ClientSession session = getRequiredSession(); + // if (options != null) { + // session.startTransaction(options); + // } else { + session.startTransaction(); + // } + } + + /** + * Commit the transaction. + */ + public void commitTransaction() { + ((Mono)(getRequiredSession().commitTransaction())).block(); + } + + /** + * Rollback (abort) the transaction. + */ + public void abortTransaction() { + ((Mono)(getRequiredSession().abortTransaction())).block(); + } + + /** + * Close a {@link ClientSession} without regard to its transactional state. + */ + void closeSession() { + + ClientSession session = getRequiredSession(); + if (session.getServerSession() != null && !session.getServerSession().isClosed()) { + session.close(); + } + } + + @Nullable + public ClientSession getSession() { + return resourceHolder != null ? resourceHolder.getSession() : null; + } + + private CouchbaseResourceHolder getRequiredResourceHolder() { + + Assert.state(resourceHolder != null, "CouchbaseResourceHolder is required but not present. o_O"); + return resourceHolder; + } + + private ClientSession getRequiredSession() { + + ClientSession session = getSession(); + Assert.state(session != null, "A Session is required but it turned out to be null."); + return session; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#isRollbackOnly() + */ + @Override + public boolean isRollbackOnly() { + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#flush() + */ + @Override + public void flush() { + TransactionSynchronizationUtils.triggerFlush(); + } + + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java new file mode 100644 index 000000000..5331cea53 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java @@ -0,0 +1,33 @@ +package org.springframework.data.couchbase.transaction; + +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.DefaultTransactionStatus; + +public class CouchbaseTransactionStatus extends DefaultTransactionStatus { + + final TransactionSynchronizationManager transactionSynchronizationManager; + /** + * Create a new {@code DefaultTransactionStatus} instance. + * + * @param transaction underlying transaction object that can hold state + * for the internal transaction implementation + * @param newTransaction if the transaction is new, otherwise participating + * in an existing transaction + * @param newSynchronization if a new transaction synchronization has been + * opened for the given transaction + * @param readOnly whether the transaction is marked as read-only + * @param debug should debug logging be enabled for the handling of this transaction? + * Caching it in here can prevent repeated calls to ask the logging system whether + * debug logging should be enabled. + * @param suspendedResources a holder for resources that have been suspended + */ + public CouchbaseTransactionStatus(Object transaction, boolean newTransaction, boolean newSynchronization, boolean readOnly, boolean debug, Object suspendedResources, TransactionSynchronizationManager sm) { + super(transaction, + newTransaction, + newSynchronization, + readOnly, + debug, + suspendedResources); + transactionSynchronizationManager = sm; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save new file mode 100644 index 000000000..aaab01580 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save @@ -0,0 +1,197 @@ +/* + * Copyright 2021 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.transaction; + +import com.couchbase.transactions.AttemptContext; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.TransactionStatus; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.reactive.TransactionCallback; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.util.Assert; + +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.TransactionsReactive; + +public class CouchbaseTransactionalOperatorNonReactive implements TransactionalOperator { + + // package org.springframework.transaction.reactive; + private static final Log logger = LogFactory.getLog(CouchbaseTransactionalOperatorNonReactive.class); + private final PlatformTransactionManager transactionManager; + private final TransactionDefinition transactionDefinition; + + Map getResultMap = new HashMap<>(); + private AttemptContext attemptContext; + + public CouchbaseTransactionalOperatorNonReactive(CouchbaseTransactionManager transactionManager) { + this(transactionManager, new CouchbaseTransactionDefinition()); + } + + public CouchbaseTransactionalOperatorNonReactive(CouchbaseTransactionManager transactionManager, + TransactionDefinition transactionDefinition) { + Assert.notNull(transactionManager, "ReactiveTransactionManager must not be null"); + Assert.notNull(transactionDefinition, "TransactionDefinition must not be null"); + this.transactionManager = transactionManager; + this.transactionDefinition = transactionDefinition; + } + + public TransactionResult execute(Function transactionLogic) { + return execute(transactionLogic, true); + } + + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default + * PerTransactionConfig. + */ + public TransactionResult execute(Function transactionLogic, + boolean commit) { + return (((CouchbaseTransactionManager) transactionManager).getTransactions().run((ctx) -> { + setAttemptContext(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport + // for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method + // and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder() + transactionLogic.apply(this); + }, null)); + } + + public TransactionResultHolder transactionResultHolder(Integer key) { + return getResultMap.get(key); + } + + public TransactionResultHolder transactionResultHolder(TransactionGetResult result) { + TransactionResultHolder holder = new TransactionResultHolder(result); + getResultMap.put(System.identityHashCode(holder), holder); + return holder; + } + + public void setAttemptContext(AttemptContext attemptContext) { + this.attemptContext = attemptContext; + } + + public AttemptContext getAttemptContext() { + return attemptContext; + } + + @Override + public Flux transactional(Flux flux) { + return execute((it -> flux); + } + + @Override + public Mono transactional(Mono mono) { + return TransactionContextManager.currentContext().flatMap(context -> { + // getCtx()/getAttemptTransActionReactive() has the atr + // atr : transactionalOpterator -> transactionDefinition -> transactionHolder -> + ((CouchbaseTransactionDefinition) transactionDefinition).setAttemptContext(getAttemptContext()); + TransactionStatus status = this.transactionManager.getTransaction(this.transactionDefinition); + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + // Need re-wrapping of ReactiveTransaction until we get hold of the exception + // through usingWhen. + return Mono.just(status) + .flatMap(it -> Mono + .usingWhen(Mono.just(it), ignore -> mono, this.transactionManager::commit, (res, err) -> Mono.empty(), + this.transactionManager::rollback) + .onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex)))); + }).contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + } + + @Override + public Flux execute(TransactionCallback action) throws TransactionException { + return TransactionContextManager.currentContext().flatMapMany(context -> { + TransactionStatus status = this.transactionManager.getTransaction(this.transactionDefinition); + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + // Need re-wrapping of ReactiveTransaction until we get hold of the exception + // through usingWhen. + return status + .flatMapMany(it -> Flux + .usingWhen(Mono.just(it), action::doInTransaction, this.transactionManager::commit, + (tx, ex) -> Mono.empty(), this.transactionManager::rollback) + .onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex)))); + }).contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + } + + private void rollbackOnException(TransactionStatus status, Throwable ex) throws TransactionException { + logger.debug("Initiating transaction rollback on application exception", ex); + this.transactionManager.rollback(status); + /*.onErrorMap((ex2) -> { + logger.error("Application exception overridden by rollback exception", ex); + if (ex2 instanceof TransactionSystemException) { + ((TransactionSystemException) ex2).initApplicationException(ex); + } + return ex2; + }); + */ + } + + /* + public TransactionDefinition getTransactionDefinition() { + return transactionDefinition; + } + */ + + public TransactionManager getTransactionManager() { + return transactionManager; + } + + public CouchbaseTemplate template(CouchbaseTemplate template) { + CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) ((CouchbaseTransactionalOperatorNonReactive) this) + .getTransactionManager()); + if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Template must use the same clientFactory as the transactionManager of the transactionalOperator " + + template); + } + return template.with(this); // template with a new couchbaseClient with txOperator + } + + public > R repository(R repo) { + if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) { + throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo); + } + CouchbaseOperations operations = (CouchbaseOperations) repo.getOperations(); + CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) (this).getTransactionManager()); + + if (operations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + repo); + } + return repo.withTransaction(this); // this returns a new repository proxy with txOperator in its threadLocal + // what if instead we returned a new repo with a new template with the txOperator? + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java new file mode 100644 index 000000000..e554352d9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java @@ -0,0 +1,303 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.java.ClusterInterface; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.lang.Nullable; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.reactive.ReactiveResourceSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +public class ReactiveCouchbaseClientUtils { + + /** + * Check if the {@link ReactiveMongoDatabaseFactory} is actually bound to a + * {@link com.mongodb.reactivestreams.client.ClientSession} that has an active transaction, or if a + * {@link org.springframework.transaction.reactive.TransactionSynchronization} has been registered for the + * {@link ReactiveMongoDatabaseFactory resource} and if the associated + * {@link com.mongodb.reactivestreams.client.ClientSession} has an + * {@link com.mongodb.reactivestreams.client.ClientSession#hasActiveTransaction() active transaction}. + * + * @param databaseFactory the resource to check transactions for. Must not be {@literal null}. + * @return a {@link Mono} emitting {@literal true} if the factory has an ongoing transaction. + */ + public static Mono isTransactionActive(ReactiveCouchbaseClientFactory databaseFactory) { + + if (databaseFactory.isTransactionActive()) { + return Mono.just(true); + } + + return TransactionSynchronizationManager.forCurrentTransaction() // + .map(it -> { + + ReactiveCouchbaseResourceHolder holder = (ReactiveCouchbaseResourceHolder) it.getResource(databaseFactory); + return holder != null && holder.hasActiveTransaction(); + }) // + .onErrorResume(NoTransactionException.class, e -> Mono.just(false)); + } + + /** + * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory} using + * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getDatabase(ReactiveCouchbaseClientFactory factory) { + return doGetCouchbaseCluster(null, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION); + } + + /** + * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory}. + *
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getDatabase(ReactiveCouchbaseClientFactory factory, + SessionSynchronization sessionSynchronization) { + return doGetCouchbaseCluster(null, factory, sessionSynchronization); + } + + public static Mono getTemplate(ReactiveCouchbaseClientFactory factory, + SessionSynchronization sessionSynchronization, CouchbaseConverter converter) { + return doGetCouchbaseTemplate(null, factory, sessionSynchronization, converter); + } + + /** + * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory + * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param dbName the name of the {@link MongoDatabase} to get. + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getDatabase(String dbName, ReactiveCouchbaseClientFactory factory) { + return doGetCouchbaseCluster(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION); + } + + /** + * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory + * factory}.
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param dbName the name of the {@link MongoDatabase} to get. + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getCluster(String dbName, ReactiveCouchbaseClientFactory factory, + SessionSynchronization sessionSynchronization) { + return doGetCouchbaseCluster(dbName, factory, sessionSynchronization); + } + + private static Mono doGetCouchbaseCluster(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization) { + + Assert.notNull(factory, "DatabaseFactory must not be null!"); + + if (sessionSynchronization == SessionSynchronization.NEVER) { + return getCouchbaseClusterOrDefault(dbName, factory); + } + + return TransactionSynchronizationManager.forCurrentTransaction() + .filter(TransactionSynchronizationManager::isSynchronizationActive) // + .flatMap(synchronizationManager -> { + + return doGetSession(synchronizationManager, factory, sessionSynchronization) // + .flatMap(it -> getCouchbaseClusterOrDefault(dbName, factory.withSession(it))); + }) // + .onErrorResume(NoTransactionException.class, e -> getCouchbaseClusterOrDefault(dbName, factory)) // hitting this + .switchIfEmpty(getCouchbaseClusterOrDefault(dbName, factory)); + } + + private static Mono doGetCouchbaseTemplate(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization, + CouchbaseConverter converter) { + + Assert.notNull(factory, "DatabaseFactory must not be null!"); + + if (sessionSynchronization == SessionSynchronization.NEVER) { + return getCouchbaseTemplateOrDefault(dbName, factory, converter); + } + + //CouchbaseResourceHolder h = (CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager + // .getResource(factory); + TransactionSynchronizationManager.forCurrentTransaction() + .flatMap((synchronizationManager) -> { System.out.println(synchronizationManager.getResource(factory)); return null; }); + + return TransactionSynchronizationManager.forCurrentTransaction() + .flatMap(x -> { System.err.println("forCurrentTransaction: getResource() : "+x.getResource(factory.getCluster().block())); return Mono.just(x);}) + .filter(TransactionSynchronizationManager::isSynchronizationActive) // + .flatMap(synchronizationManager -> { + return doGetSession(synchronizationManager, factory, sessionSynchronization) // + .flatMap(it -> getCouchbaseTemplateOrDefault(dbName, factory.withSession(it), converter)); // rx TxMgr + }) // + .onErrorResume(NoTransactionException.class, + e -> { System.err.println("noCurrentTransaction: "); return getCouchbaseTemplateOrDefault(dbName, + getNonReactiveSession(factory) != null ? factory.withSession(getNonReactiveSession(factory)) : factory, + converter);}) // blocking TxMgr + .switchIfEmpty(getCouchbaseTemplateOrDefault(dbName, factory, converter)); + } + + private static ClientSession getNonReactiveSession(ReactiveCouchbaseClientFactory factory) { + CouchbaseResourceHolder h = ((CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager + .getResource(factory.getCluster().block())); + if( h == null){ + h = ((CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager + .getResource(factory));// MN's CouchbaseTransactionManager + } + System.err.println("getNonreactiveSession: "+ h); + return h != null ? h.getSession() : null; + } + + private static Mono getCouchbaseClusterOrDefault(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory) { + return StringUtils.hasText(dbName) ? factory.getCluster() : factory.getCluster(); + } + + private static Mono getCouchbaseTemplateOrDefault(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory, CouchbaseConverter converter) { + return Mono.just(new ReactiveCouchbaseTemplate(factory, converter)); + } + + private static Mono doGetSession(TransactionSynchronizationManager synchronizationManager, + ReactiveCouchbaseClientFactory dbFactory, SessionSynchronization sessionSynchronization) { + + final ReactiveCouchbaseResourceHolder registeredHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager + .getResource(dbFactory.getCluster().block()); // make sure this wasn't saved under the wrong key!!! + + // check for native MongoDB transaction + if (registeredHolder != null + && (registeredHolder.hasSession() || registeredHolder.isSynchronizedWithTransaction())) { + System.err.println("doGetSession: got: "+registeredHolder.getSession()); + return registeredHolder.hasSession() ? Mono.just(registeredHolder.getSession()) + : createClientSession(dbFactory).map(registeredHolder::setSessionIfAbsent); + } + + if (SessionSynchronization.ON_ACTUAL_TRANSACTION.equals(sessionSynchronization)) { + System.err.println("doGetSession: ON_ACTUAL_TRANSACTION -> empty()"); + return Mono.empty(); + } + + System.err.println("doGetSession: createClientSession()"); + + // init a non native MongoDB transaction by registering a MongoSessionSynchronization + return createClientSession(dbFactory).map(session -> { + + ReactiveCouchbaseResourceHolder newHolder = new ReactiveCouchbaseResourceHolder(session, dbFactory); + newHolder.getRequiredSession().startTransaction(); + + synchronizationManager + .registerSynchronization(new CouchbaseSessionSynchronization(synchronizationManager, newHolder, dbFactory)); + newHolder.setSynchronizedWithTransaction(true); + synchronizationManager.bindResource(dbFactory, newHolder); + + return newHolder.getSession(); + }); + } + + private static Mono createClientSession(ReactiveCouchbaseClientFactory dbFactory) { + return dbFactory.getSession(ClientSessionOptions.builder().causallyConsistent(true).build()); + } + + /** + * MongoDB specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when + * participating in a non-native MongoDB transaction, such as a R2CBC transaction. + * + * @author Mark Paluch + * @since 2.2 + */ + private static class CouchbaseSessionSynchronization + extends ReactiveResourceSynchronization { + + private final ReactiveCouchbaseResourceHolder resourceHolder; + + CouchbaseSessionSynchronization(TransactionSynchronizationManager synchronizationManager, + ReactiveCouchbaseResourceHolder resourceHolder, ReactiveCouchbaseClientFactory dbFactory) { + + super(resourceHolder, dbFactory, synchronizationManager); + this.resourceHolder = resourceHolder; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#shouldReleaseBeforeCompletion() + */ + @Override + protected boolean shouldReleaseBeforeCompletion() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#processResourceAfterCommit(java.lang.Object) + */ + @Override + protected Mono processResourceAfterCommit(ReactiveCouchbaseResourceHolder resourceHolder) { + + if (isTransactionActive(resourceHolder)) { + return Mono.from(resourceHolder.getRequiredSession().commitTransaction()); + } + + return Mono.empty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#afterCompletion(int) + */ + @Override + public Mono afterCompletion(int status) { + + return Mono.defer(() -> { + + if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) { + + return Mono.from(resourceHolder.getRequiredSession().abortTransaction()) // + .then(super.afterCompletion(status)); + } + + return super.afterCompletion(status); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#releaseResource(java.lang.Object, java.lang.Object) + */ + @Override + protected Mono releaseResource(ReactiveCouchbaseResourceHolder resourceHolder, Object resourceKey) { + + return Mono.fromRunnable(() -> { + if (resourceHolder.hasActiveSession()) { + resourceHolder.getRequiredSession().close(); + } + }); + } + + private boolean isTransactionActive(ReactiveCouchbaseResourceHolder resourceHolder) { + + if (!resourceHolder.hasSession()) { + return false; + } + + return resourceHolder.getRequiredSession().hasActiveTransaction(); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java new file mode 100644 index 000000000..f7c76ff2a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java @@ -0,0 +1,155 @@ +/* + * Copyright 2019-2021 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.transaction; + +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; + +/** + * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveCouchbaseTransactionManager} binds + * instances of this class to the subscriber context. + *

+ * Note: Intended for internal usage only. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.2 + * @see ReactiveCouchbaseTransactionManager + * @see ReactiveCouchbaseTemplate + */ +public class ReactiveCouchbaseResourceHolder extends ResourceHolderSupport { + + private @Nullable ClientSession session; // which holds the atr + private ReactiveCouchbaseClientFactory databaseFactory; + + /** + * Create a new {@link ReactiveCouchbaseResourceHolder} for a given {@link ClientSession session}. + * + * @param session the associated {@link ClientSession}. Can be {@literal null}. + * @param databaseFactory the associated {@link CouchbaseClientFactory}. must not be {@literal null}. + */ + public ReactiveCouchbaseResourceHolder(@Nullable ClientSession session, ReactiveCouchbaseClientFactory databaseFactory) { + + this.session = session; + this.databaseFactory = databaseFactory; + } + + /** + * @return the associated {@link ClientSession}. Can be {@literal null}. + */ + @Nullable + ClientSession getSession() { + return session; + } + + /** + * @return the required associated {@link ClientSession}. + * @throws IllegalStateException if no session is associated. + */ + ClientSession getRequiredSession() { + + ClientSession session = getSession(); + + if (session == null) { + throw new IllegalStateException("No ClientSession associated"); + } + return session; + } + + /** + * @return the associated {@link CouchbaseClientFactory}. + */ + ReactiveCouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + /** + * Set the {@link ClientSession} to guard. + * + * @param session can be {@literal null}. + */ + void setSession(@Nullable ClientSession session) { + this.session = session; + } + + /** + * @return {@literal true} if session is not {@literal null}. + */ + boolean hasSession() { + return session != null; + } + + /** + * If the {@link ReactiveCouchbaseResourceHolder} is {@link #hasSession() not already associated} with a + * {@link ClientSession} the given value is {@link #setSession(ClientSession) set} and returned, otherwise the current + * bound session is returned. + * + * @param session + * @return + */ + @Nullable + ClientSession setSessionIfAbsent(@Nullable ClientSession session) { + + if (!hasSession()) { + setSession(session); + } + + return session; + } + + /** + * @return {@literal true} if the session is active and has not been closed. + */ + boolean hasActiveSession() { + + if (!hasSession()) { + return false; + } + + return hasServerSession() && !getRequiredSession().getServerSession().isClosed(); + } + + /** + * @return {@literal true} if the session has an active transaction. + * @see #hasActiveSession() + */ + boolean hasActiveTransaction() { + + if (!hasActiveSession()) { + return false; + } + + return getRequiredSession().hasActiveTransaction(); + } + + /** + * @return {@literal true} if the {@link ClientSession} has a {link com.mongodb.session.ServerSession} associated that + * is accessible via {@link ClientSession#getServerSession()}. + */ + boolean hasServerSession() { + + try { + return getRequiredSession().getServerSession() != null; + } catch (IllegalStateException serverSessionClosed) { + // ignore + } + + return false; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java new file mode 100644 index 000000000..b390c8151 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java @@ -0,0 +1,540 @@ +/* + * Copyright 2019-2021 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.transaction; + +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.lang.Nullable; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; +import org.springframework.transaction.reactive.GenericReactiveTransaction; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.TransactionQueryOptions; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; + +/** + * A {@link org.springframework.transaction.ReactiveTransactionManager} implementation that manages + * {@link ClientSession} based transactions for a single {@link CouchbaseClientFactory}. + *

+ * Binds a {@link ClientSession} from the specified {@link CouchbaseClientFactory} to the subscriber + * {@link reactor.util.context.Context}. + *

+ * {@link org.springframework.transaction.TransactionDefinition#isReadOnly() Readonly} transactions operate on a + * {@link ClientSession} and enable causal consistency, and also {@link ClientSession#startTransaction() start}, + * {@link ClientSession#commitTransaction() commit} or {@link ClientSession#abortTransaction() abort} a transaction. + *

+ * Application code is required to retrieve the {link com.xxxxxxx.reactivestreams.client.MongoDatabase} via {link + * org.springframework.data.xxxxxxx.ReactiveMongoDatabaseUtils#getDatabase(CouchbaseClientFactory)} instead of a + * standard {@link org.springframework.data.couchbase.CouchbaseClientFactory#getCluster()} call. Spring classes such as + * {@link org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate} use this strategy implicitly. + *

+ * By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. You can override + * {@link #doCommit(TransactionSynchronizationManager, ReactiveCouchbaseTransactionObject)} to implement the + * Retry Commit Operation + * behavior as outlined in the XxxxxxXX reference manual. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.2 + * @see XxxxxxXX Transaction Documentation see + * ReactiveMongoDatabaseUtils#getDatabase(CouchbaseClientFactory, SessionSynchronization) + */ +public class ReactiveCouchbaseTransactionManager extends AbstractReactiveTransactionManager + implements InitializingBean { + + private @Nullable ReactiveCouchbaseClientFactory databaseFactory; // (why) does this need to be reactive? + private @Nullable Transactions transactions; // This is the com.couchbase.transactions object + private @Nullable TransactionConfig config; + + /** + * Create a new {@link ReactiveCouchbaseTransactionManager} for bean-style usage. + *

+ * Note:The {@link org.springframework.data.couchbase.CouchbaseClientFactory db factory} has to be + * {@link #setDatabaseFactory(ReactiveCouchbaseClientFactory)} set} before using the instance. Use this constructor to prepare + * a {@link ReactiveCouchbaseTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}. + *

+ * Optionally it is possible to set default {@link TransactionQueryOptions transaction options} defining {link + * com.xxxxxxx.ReadConcern} and {link com.xxxxxxx.WriteConcern}. + * + * @see #setDatabaseFactory(ReactiveCouchbaseClientFactory) + */ + public ReactiveCouchbaseTransactionManager() {} + + /** + * Create a new {@link ReactiveCouchbaseTransactionManager} obtaining sessions from the given + * {@link CouchbaseClientFactory} applying the given {@link TransactionQueryOptions options}, if present, when + * starting a new transaction. + * + * @param databaseFactory must not be {@literal null}. + * @param transactions - couchbase Transactions object + */ + public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databaseFactory, + @Nullable Transactions transactions) { + Assert.notNull(databaseFactory, "DatabaseFactory must not be null!"); + this.databaseFactory = databaseFactory; // should be a clone? TransactionSynchronizationManager binds objs to it + this.transactions = transactions; + System.err.println("ReactiveCouchbaseTransactionManager : created Transactions: " + transactions); + } + + /* + public ReactiveCouchbaseTransactionManager(CouchbaseClientFactory databaseFactory, + @Nullable Transactions transactions) { + Assert.notNull(databaseFactory, "DatabaseFactory must not be null!"); + this.databaseFactory = null; // databaseFactory; // should be a clone? TransactionSynchronizationManager binds objs to it + this.transactions = transactions; + System.err.println("ReactiveCouchbaseTransactionManager : created Transactions: " + transactions); + } +*/ + public Transactions getTransactions() { + System.err.println("ReactiveCouchbaseTransactionManager.getTransactions() : " + transactions); + return transactions; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doGetTransaction(org.springframework.transaction.reactive.TransactionSynchronizationManager) + */ + @Override + protected Object doGetTransaction(TransactionSynchronizationManager synchronizationManager) + throws TransactionException { + // creation of a new ReactiveCouchbaseTransactionObject (i.e. transaction). + // with an attempt to get the resourceHolder from the synchronizationManager + ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager + .getResource(getRequiredDatabaseFactory().getCluster().block()); + //TODO ACR from couchbase + //resourceHolder.getSession().setAttemptContextReactive(null); + return new ReactiveCouchbaseTransactionObject(resourceHolder); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(java.lang.Object) + */ + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractCouchbaseTransaction(transaction).hasResourceHolder(); + } + + /** + * doBegin() attaches the atr from the transactionOperator in the transactionDefinition to the transaction (via + * resourceHolder -> Clientsession) (non-Javadoc) + * + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doBegin(org.springframework.transaction.reactive.TransactionSynchronizationManager, + * java.lang.Object, org.springframework.transaction.TransactionDefinition) + */ + @Override + protected Mono doBegin(TransactionSynchronizationManager synchronizationManager, Object transaction, + TransactionDefinition definition) throws TransactionException { + + return Mono.defer(() -> { + + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); + + Mono holder = newResourceHolder(definition, + ClientSessionOptions.builder().causallyConsistent(true).build()); + return holder.doOnNext(resourceHolder -> { + couchbaseTransactionObject.setResourceHolder(resourceHolder); + + if (logger.isDebugEnabled()) { + logger.debug( + String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession()))); + } + + }).doOnNext(resourceHolder -> { + + couchbaseTransactionObject.startTransaction(config); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession()))); + } + + })// + .onErrorMap(ex -> new TransactionSystemException( + String.format("Could not start Couchbase transaction for session %s.", + debugString(couchbaseTransactionObject.getSession())), + ex)) + .doOnSuccess(resourceHolder -> { + System.err.println("ReactiveCouchbaseTransactionManager: "+this); + System.err.println("bindResource: "+getRequiredDatabaseFactory().getCluster().block()+" value: "+resourceHolder); + synchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster().block(), resourceHolder); + }).then(); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSuspend(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object) + */ + @Override + protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) + throws TransactionException { + + return Mono.fromSupplier(() -> { + + ReactiveCouchbaseTransactionObject mongoTransactionObject = extractCouchbaseTransaction(transaction); + mongoTransactionObject.setResourceHolder(null); + + return synchronizationManager.unbindResource(getRequiredDatabaseFactory()); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doResume(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object, java.lang.Object) + */ + @Override + protected Mono doResume(TransactionSynchronizationManager synchronizationManager, @Nullable Object transaction, + Object suspendedResources) { + return Mono + .fromRunnable(() -> synchronizationManager.bindResource(getRequiredDatabaseFactory(), suspendedResources)); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCommit(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected final Mono doCommit(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + return Mono.defer(() -> { + + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to doCommit transaction for session %s.", + debugString(couchbaseTransactionObject.getSession()))); + } + + return doCommit(synchronizationManager, couchbaseTransactionObject).onErrorMap(ex -> { + return new TransactionSystemException(String.format("Could not commit Couchbase transaction for session %s.", + debugString(couchbaseTransactionObject.getSession())), ex); + }); + }); + } + + /** + * Customization hook to perform an actual commit of the given transaction.
+ * If a commit operation encounters an error, the XxxxxxXX driver throws a {@link CouchbaseException} holding + * {@literal error labels}.
+ * By default those labels are ignored, nevertheless one might check for {@link CouchbaseException transient commit + * errors labels} and retry the the commit. + * + * @param synchronizationManager reactive synchronization manager. + * @param transactionObject never {@literal null}. + */ + protected Mono doCommit(TransactionSynchronizationManager synchronizationManager, + ReactiveCouchbaseTransactionObject transactionObject) { + return transactionObject.commitTransaction(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doRollback(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected Mono doRollback(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) { + + return Mono.defer(() -> { + + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to abort transaction for session %s.", + debugString(couchbaseTransactionObject.getSession()))); + } + + return couchbaseTransactionObject.abortTransaction().onErrorResume(CouchbaseException.class, ex -> { + return Mono.error(new TransactionSystemException(String.format("Could not abort transaction for session %s.", + debugString(couchbaseTransactionObject.getSession())), ex)); + }); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + + return Mono.fromRunnable(() -> { + ReactiveCouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status); + transactionObject.getRequiredResourceHolder().setRollbackOnly(); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCleanupAfterCompletion(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object) + */ + @Override + protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager, + Object transaction) { + + Assert.isInstanceOf(ReactiveCouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", + ReactiveCouchbaseTransactionObject.class, transaction.getClass())); + + return Mono.fromRunnable(() -> { + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = (ReactiveCouchbaseTransactionObject) transaction; + + // Remove the connection holder from the thread. + synchronizationManager.unbindResource(getRequiredDatabaseFactory().getCluster().block()); + couchbaseTransactionObject.getRequiredResourceHolder().clear(); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to release Session %s after transaction.", + debugString(couchbaseTransactionObject.getSession()))); + } + + couchbaseTransactionObject.closeSession(); + }); + } + + /** + * Set the {@link CouchbaseClientFactory} that this instance should manage transactions for. + * + * @param databaseFactory must not be {@literal null}. + */ + public void setDatabaseFactory(ReactiveCouchbaseClientFactory databaseFactory) { + + Assert.notNull(databaseFactory, "DatabaseFactory must not be null!"); + this.databaseFactory = databaseFactory; + } + + /** + * Set the {@link TransactionConfig} to be applied when starting transactions. + * + * @param config can be {@literal null}. + */ + public void setConfig(@Nullable TransactionConfig config) { + this.config = config; + } + + /** + * Get the {@link CouchbaseClientFactory} that this instance manages transactions for. + * + * @return can be {@literal null}. + */ + @Nullable + public ReactiveCouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + getRequiredDatabaseFactory(); + } + + private Mono newResourceHolder(TransactionDefinition definition, + ClientSessionOptions options) { + + ReactiveCouchbaseClientFactory dbFactory = getRequiredDatabaseFactory(); + // TODO MSR : config should be derived from config that was used for `transactions` + getTransactions().reactive(); + TransactionConfig config = transactions.reactive().config(); + Mono sess = Mono.just(dbFactory.getSession(options, transactions, config , null/* TODO */)); + return sess.map(session -> new ReactiveCouchbaseResourceHolder(session, dbFactory)); + } + + /** + * @throws IllegalStateException if {@link #databaseFactory} is {@literal null}. + */ + private ReactiveCouchbaseClientFactory getRequiredDatabaseFactory() { + Assert.state(databaseFactory != null, + "ReactiveCouchbaseTransactionManager operates upon a CouchbaseClientFactory. Did you forget to provide one? It's required."); + return databaseFactory; + } + + private static ReactiveCouchbaseTransactionObject extractCouchbaseTransaction(Object transaction) { + + Assert.isInstanceOf(ReactiveCouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", + ReactiveCouchbaseTransactionObject.class, transaction.getClass())); + + return (ReactiveCouchbaseTransactionObject) transaction; + } + + private static ReactiveCouchbaseTransactionObject extractCouchbaseTransaction(GenericReactiveTransaction status) { + + Assert.isInstanceOf(ReactiveCouchbaseTransactionObject.class, status.getTransaction(), + () -> String.format("Expected to find a %s but it turned out to be %s.", + ReactiveCouchbaseTransactionObject.class, status.getTransaction().getClass())); + + return (ReactiveCouchbaseTransactionObject) status.getTransaction(); + } + + private static String debugString(@Nullable ClientSession session) { + + if (session == null) { + return "null"; + } + + String debugString = String.format("[%s@%s ", ClassUtils.getShortName(session.getClass()), + Integer.toHexString(session.hashCode())); + + try { + if (session.getServerSession() != null) { + debugString += String.format("id = %s, ", session.getServerSession().getIdentifier()); + debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent()); + debugString += String.format("txActive = %s, ", session.hasActiveTransaction()); + debugString += String.format("txNumber = %d, ", session.getServerSession().getTransactionNumber()); + debugString += String.format("closed = %d, ", session.getServerSession().isClosed()); + debugString += String.format("clusterTime = %s", session.getClusterTime()); + } else { + debugString += "id = n/a"; + debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent()); + debugString += String.format("txActive = %s, ", session.hasActiveTransaction()); + debugString += String.format("clusterTime = %s", session.getClusterTime()); + } + } catch (RuntimeException e) { + debugString += String.format("error = %s", e.getMessage()); + } + + debugString += "]"; + + return debugString; + } + + /** + * Couchbase specific transaction object, representing a {@link ReactiveCouchbaseResourceHolder}. Used as transaction + * object by {@link ReactiveCouchbaseTransactionManager}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.2 + * @see ReactiveCouchbaseResourceHolder + */ + protected static class ReactiveCouchbaseTransactionObject implements SmartTransactionObject { + + public @Nullable ReactiveCouchbaseResourceHolder resourceHolder; + + ReactiveCouchbaseTransactionObject(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * Set the {@link ReactiveCouchbaseResourceHolder}. + * + * @param resourceHolder can be {@literal null}. + */ + void setResourceHolder(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * @return {@literal true} if a {@link ReactiveCouchbaseResourceHolder} is set. + */ + final boolean hasResourceHolder() { + return resourceHolder != null; + } + + /** + * Start a XxxxxxXX transaction optionally given {@link TransactionQueryOptions}. + * + * @param options can be {@literal null} + */ + void startTransaction(@Nullable TransactionConfig options) { + + ClientSession session = getRequiredSession(); + if (options != null) { + session.startTransaction(options); + } else { + session.startTransaction(); + } + } + + /** + * Commit the transaction. + */ + public Mono commitTransaction() { + return (Mono)(getRequiredSession().commitTransaction()); + } + + /** + * Rollback (abort) the transaction. + */ + public Mono abortTransaction() { + return (Mono)getRequiredSession().abortTransaction(); + } + + /** + * Close a {@link ClientSession} without regard to its transactional state. + */ + void closeSession() { + + ClientSession session = getRequiredSession(); + if (session.getServerSession() != null && !session.getServerSession().isClosed()) { + session.close(); + } + } + + @Nullable + public ClientSession getSession() { + return resourceHolder != null ? resourceHolder.getSession() : null; + } + + private ReactiveCouchbaseResourceHolder getRequiredResourceHolder() { + + Assert.state(resourceHolder != null, "ReactiveMongoResourceHolder is required but not present. o_O"); + return resourceHolder; + } + + private ClientSession getRequiredSession() { + + ClientSession session = getSession(); + Assert.state(session != null, "A Session is required but it turned out to be null."); + return session; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#isRollbackOnly() + */ + @Override + public boolean isRollbackOnly() { + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#flush() + */ + @Override + public void flush() { + throw new UnsupportedOperationException("flush() not supported"); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ServerSession.java b/src/main/java/org/springframework/data/couchbase/transaction/ServerSession.java new file mode 100644 index 000000000..d53cf1f0a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ServerSession.java @@ -0,0 +1,19 @@ +package org.springframework.data.couchbase.transaction; + +/** + * used only by ClientSession.getServerSession() - which returns null + */ + +public interface ServerSession { + String getIdentifier(); + + long getTransactionNumber(); + + long advanceTransactionNumber(); + + boolean isClosed(); + + void markDirty(); + + boolean isMarkedDirty(); +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java new file mode 100644 index 000000000..69d9a77a8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java @@ -0,0 +1,212 @@ +/* + * Copyright 2018-2021 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.transaction; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodClassKey; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; + +/** + * {@link MethodInterceptor} implementation looking up and invoking an alternative target method having + * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base. + *
+ * The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself + * like (eg. TODO) and decorate them + * if not already proxied. + * + * @param Type of the actual Mongo Database. + * @param Type of the actual Mongo Collection. + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ +public class SessionAwareMethodInterceptor implements MethodInterceptor { + + private static final MethodCache METHOD_CACHE = new MethodCache(); + + private final ClientSession session; + private final ClientSessionOperator collectionDecorator; + private final ClientSessionOperator databaseDecorator; + private final Object target; + private final Class targetType; + private final Class collectionType; + private final Class databaseType; + private final Class sessionType; + + /** + * Create a new SessionAwareMethodInterceptor for given target. + * + * @param session the {@link ClientSession} to be used on invocation. + * @param target the original target object. + * @param databaseType the MongoDB database type + * @param databaseDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive + * {@code MongoDatabase}. + * @param collectionType the MongoDB collection type. + * @param collectionDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive + * {@code MongoCollection}. + * @param target object type. + */ + public SessionAwareMethodInterceptor(ClientSession session, T target, Class sessionType, + Class databaseType, ClientSessionOperator databaseDecorator, Class collectionType, + ClientSessionOperator collectionDecorator) { + + Assert.notNull(session, "ClientSession must not be null!"); + Assert.notNull(target, "Target must not be null!"); + Assert.notNull(sessionType, "SessionType must not be null!"); + Assert.notNull(databaseType, "Database type must not be null!"); + Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null!"); + Assert.notNull(collectionType, "Collection type must not be null!"); + Assert.notNull(collectionDecorator, "Collection ClientSessionOperator must not be null!"); + + this.session = session; + this.target = target; + this.databaseType = ClassUtils.getUserClass(databaseType); + this.collectionType = ClassUtils.getUserClass(collectionType); + this.collectionDecorator = collectionDecorator; + this.databaseDecorator = databaseDecorator; + + this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType; + this.sessionType = sessionType; + } + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor(org.aopalliance.intercept.MethodInvocation) + */ + @Nullable + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + + if (requiresDecoration(methodInvocation.getMethod())) { + + Object target = methodInvocation.proceed(); + if (target instanceof Proxy) { + return target; + } + + return decorate(target); + } + + if (!requiresSession(methodInvocation.getMethod())) { + return methodInvocation.proceed(); + } + + Optional targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType, sessionType); + + return !targetMethod.isPresent() ? methodInvocation.proceed() + : ReflectionUtils.invokeMethod(targetMethod.get(), target, + prependSessionToArguments(session, methodInvocation)); + } + + private boolean requiresDecoration(Method method) { + + return ClassUtils.isAssignable(databaseType, method.getReturnType()) + || ClassUtils.isAssignable(collectionType, method.getReturnType()); + } + + @SuppressWarnings("unchecked") + protected Object decorate(Object target) { + + return ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseDecorator.apply(session, target) + : collectionDecorator.apply(session, target); + } + + private static boolean requiresSession(Method method) { + + if (method.getParameterCount() == 0 + || !ClassUtils.isAssignable(ClientSession.class, method.getParameterTypes()[0])) { + return true; + } + + return false; + } + + private static Object[] prependSessionToArguments(ClientSession session, MethodInvocation invocation) { + + Object[] args = new Object[invocation.getArguments().length + 1]; + + args[0] = session; + System.arraycopy(invocation.getArguments(), 0, args, 1, invocation.getArguments().length); + + return args; + } + + /** + * Simple {@link Method} to {@link Method} caching facility for {@link ClientSession} overloaded targets. + * + * @since 2.1 + * @author Christoph Strobl + */ + static class MethodCache { + + private final ConcurrentReferenceHashMap> cache = new ConcurrentReferenceHashMap<>(); + + /** + * Lookup the target {@link Method}. + * + * @param method + * @param targetClass + * @return + */ + Optional lookup(Method method, Class targetClass, Class sessionType) { + + return cache.computeIfAbsent(new MethodClassKey(method, targetClass), + val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType))); + } + + @Nullable + private Method findTargetWithSession(Method sourceMethod, Class targetType, + Class sessionType) { + + Class[] argTypes = sourceMethod.getParameterTypes(); + Class[] args = new Class[argTypes.length + 1]; + args[0] = sessionType; + System.arraycopy(argTypes, 0, args, 1, argTypes.length); + + return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args); + } + + /** + * Check whether the cache contains an entry for {@link Method} and {@link Class}. + * + * @param method + * @param targetClass + * @return + */ + boolean contains(Method method, Class targetClass) { + return cache.containsKey(new MethodClassKey(method, targetClass)); + } + } + + /** + * Represents an operation upon two operands of the same type, producing a result of the same type as the operands + * accepting {@link ClientSession}. This is a specialization of {@link BiFunction} for the case where the operands and + * the result are all of the same type. + * + * @param the type of the operands and result of the operator + */ + public interface ClientSessionOperator extends BiFunction {} +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/SessionSynchronization.java b/src/main/java/org/springframework/data/couchbase/transaction/SessionSynchronization.java new file mode 100644 index 000000000..d62633ba9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/SessionSynchronization.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018-2021 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.transaction; + +/** + * TODO MSR not used + * {@link SessionSynchronization} is used along with {@link org.springframework.data.couchbase.core.CouchbaseTemplate} to + * define in which type of transactions to participate if any. + * + * @author Michael Reiche + */ +public enum SessionSynchronization { + + /** + * Synchronize with any transaction even with empty transactions and initiate a MongoDB transaction when doing so by + * registering a MongoDB specific {@link org.springframework.transaction.support.ResourceHolderSynchronization}. + */ + ALWAYS, + + /** + * Synchronize with native MongoDB transactions initiated via {@link ReactiveCouchbaseTransactionManager}. + */ + ON_ACTUAL_TRANSACTION, + NEVER; + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java new file mode 100644 index 000000000..86b23d9c0 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021 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.transaction; + +public class TransactionOptions { +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java new file mode 100644 index 000000000..e3db6bd1a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java @@ -0,0 +1,137 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.transactions.AttemptContextReactiveAccessor; +import com.couchbase.transactions.TransactionContext; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.TransactionsReactive; +import com.couchbase.transactions.config.MergedTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfig; +import com.couchbase.transactions.config.PerTransactionConfigBuilder; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; +import com.couchbase.transactions.error.external.TransactionOperationFailed; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +public class TransactionsWrapper { + Transactions transactions; + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; + + public TransactionsWrapper(Transactions transactions, ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory){ + this.transactions = transactions; + this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory; + } + + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default PerTransactionConfig. + */ + public Mono reactive(Function> transactionLogic) { + // TODO long duration for debugger + Duration duration = Duration.ofMinutes(20); + System.err.println("tx duration of "+duration); + return run(transactionLogic, PerTransactionConfigBuilder.create().expirationTime(duration).build()); + } + + public Mono run(Function> transactionLogic, + PerTransactionConfig perConfig) { + TransactionConfig config = TransactionConfigBuilder.create().build(); + + ClusterEnvironment env = ClusterEnvironment.builder().build(); + return Mono.defer(() -> { + MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); + + TransactionContext overall = + new TransactionContext(env.requestTracer(), + env.eventBus(), + UUID.randomUUID().toString(), + now(), + Duration.ZERO, + merged); + AtomicReference startTime = new AtomicReference<>(0L); + + Mono ob = Mono.fromCallable(() -> { + String txnId = UUID.randomUUID().toString(); + overall.LOGGER.info(configDebug(config, perConfig)); + return transactions.reactive().createAttemptContext(overall, merged, txnId); + }).flatMap(ctx -> { + + AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", + overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); + +/* begin spring-data-couchbase transaction 1/2 */ + ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); +/* end spring-data-couchbase transaction 1/2 */ + Mono result = transactionLogic.apply(ctx); + result + .onErrorResume(err -> { + AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); + logElidedStacktrace(ctx, err); + + return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); + }) + .thenReturn(ctx); + return result.then(Mono.just(synchronizationManager)); + }); + /* begin spring-data-couchbase transaction 2/2 */ + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(Mono.just(ctx)); +/* end spring-data-couchbase transaction 2/2 */ + }).doOnSubscribe(v -> startTime.set(System.nanoTime())) + .doOnNext(v -> AttemptContextReactiveAccessor.getLogger(v).trace(v.attemptId(), "finished attempt %d in %sms", + overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); + + return transactions.reactive().executeTransaction(merged, overall, ob) + .doOnNext(v -> overall.span().finish()) + .doOnError(err -> overall.span().failWith(err)); + }); + } + + private void logElidedStacktrace(AttemptContextReactive ctx, Throwable err) { + transactions.reactive().logElidedStacktrace(ctx, err); + } + + private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { + return transactions.reactive().configDebug(config, perConfig); + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + + private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, + ReactiveTransaction status, TransactionDefinition definition) { + + // if (status.isNewSynchronization()) { + synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); + synchronizationManager.setCurrentTransactionIsolationLevel( + definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() + : null); + synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); + synchronizationManager.setCurrentTransactionName(definition.getName()); + synchronizationManager.initSynchronization(); + // } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java b/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java new file mode 100644 index 000000000..a82cdf333 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java @@ -0,0 +1,26 @@ + +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package org.springframework.data.couchbase.transaction.internal; + +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.TransactionOptions; + +public interface AsyncClientSession extends ClientSession { + boolean hasActiveTransaction(); + + boolean notifyMessageSent(); + + TransactionOptions getTransactionOptions(); + + void startTransaction(); + + void startTransaction(TransactionOptions var1); + + void commitTransaction(SingleResultCallback var1); + + void abortTransaction(SingleResultCallback var1); +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save b/src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save new file mode 100644 index 000000000..717bb662d --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save @@ -0,0 +1,170 @@ + + +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package org.springframework.data.couchbase.transaction.internal; + +import com.couchbase.client.java.Scope; +import com.mongodb.ClientSessionOptions; +import com.mongodb.MongoClientException; +import com.mongodb.ServerAddress; +import com.mongodb.assertions.Assertions; +import com.mongodb.internal.binding.ReferenceCounted; +import com.mongodb.lang.Nullable; +import com.mongodb.session.ClientSession; +import com.mongodb.session.ServerSession; +import org.bson.BsonDocument; +import org.bson.BsonTimestamp; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.lang.Nullable; +import reactor.core.publisher.Mono; + +public class BaseClientSessionImpl implements ClientSession { + private static final String CLUSTER_TIME_KEY = "clusterTime"; + private final ServerSessionPool serverSessionPool; + private final ServerSession serverSession; + private final Object originator; + private final ClientSessionOptions options; + private long clusterTime; + private long operationTime; + private long snapshotTimestamp; + private ServerAddress pinnedServerAddress; + private BsonDocument recoveryToken; + private ReferenceCounted transactionContext; + private volatile boolean closed; + + public BaseClientSessionImpl(ServerSessionPool serverSessionPool, Object originator, ClientSessionOptions options) { + this.serverSessionPool = serverSessionPool; + this.serverSession = serverSessionPool.get(); + this.originator = originator; + this.options = options; + this.pinnedServerAddress = null; + this.closed = false; + } + + @Nullable + public ServerAddress getPinnedServerAddress() { + return this.pinnedServerAddress; + } + + public Object getTransactionContext() { + return this.transactionContext; + } + + public void setTransactionContext(ServerAddress address, Object transactionContext) { + Assertions.assertTrue(transactionContext instanceof ReferenceCounted); + this.pinnedServerAddress = address; + this.transactionContext = (ReferenceCounted)transactionContext; + this.transactionContext.retain(); + } + + public void clearTransactionContext() { + this.pinnedServerAddress = null; + if (this.transactionContext != null) { + this.transactionContext.release(); + this.transactionContext = null; + } + + } + + public BsonDocument getRecoveryToken() { + return this.recoveryToken; + } + + public void setRecoveryToken(BsonDocument recoveryToken) { + this.recoveryToken = recoveryToken; + } + + public ClientSessionOptions getOptions() { + return this.options; + } + + public boolean isCausallyConsistent() { + Boolean causallyConsistent = this.options.isCausallyConsistent(); + return causallyConsistent == null ? true : causallyConsistent; + } + + public Object getOriginator() { + return this.originator; + } + + public long getClusterTime() { + return this.clusterTime; + } + + public long getOperationTime() { + return this.operationTime; + } + + @Override + public Mono getScope() { + return null; + } + + public ServerSession getServerSession() { + Assertions.isTrue("open", !this.closed); + return this.serverSession; + } + + public void advanceOperationTime(BsonTimestamp newOperationTime) { + Assertions.isTrue("open", !this.closed); + this.operationTime = this.greaterOf(newOperationTime); + } + + public void advanceClusterTime(BsonDocument newClusterTime) { + Assertions.isTrue("open", !this.closed); + this.clusterTime = this.greaterOf(newClusterTime); + } + + public void setSnapshotTimestamp(BsonTimestamp snapshotTimestamp) { + Assertions.isTrue("open", !this.closed); + if (snapshotTimestamp != null) { + if (this.snapshotTimestamp != null && !snapshotTimestamp.equals(this.snapshotTimestamp)) { + throw new MongoClientException("Snapshot timestamps should not change during the lifetime of the session. Current timestamp is " + this.snapshotTimestamp + ", and attempting to set it to " + snapshotTimestamp); + } + + this.snapshotTimestamp = snapshotTimestamp; + } + + } + + @Nullable + public BsonTimestamp getSnapshotTimestamp() { + Assertions.isTrue("open", !this.closed); + return this.snapshotTimestamp; + } + + private BsonDocument greaterOf(BsonDocument newClusterTime) { + if (newClusterTime == null) { + return this.clusterTime; + } else if (this.clusterTime == null) { + return newClusterTime; + } else { + return newClusterTime.getTimestamp("clusterTime").compareTo(this.clusterTime.getTimestamp("clusterTime")) > 0 ? newClusterTime : this.clusterTime; + } + } + + private long greaterOf(long newOperationTime) { + if (newOperationTime == 0) { + return this.operationTime; + } else if (this.operationTime == 0) { + return newOperationTime; + } else { + return newOperationTime > this.operationTime ? newOperationTime : this.operationTime; + } + } + + public void close() { + if (!this.closed) { + this.closed = true; + this.serverSessionPool.release(this.serverSession); + this.clearTransactionContext(); + } + + } +} + diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save b/src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save new file mode 100644 index 000000000..368c6efc8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save @@ -0,0 +1,241 @@ + +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * http://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.transaction.internal; + +import com.mongodb.ClientSessionOptions; +import com.mongodb.MongoClientException; +import com.mongodb.MongoException; +import com.mongodb.MongoInternalException; +import com.mongodb.ReadConcern; +import com.mongodb.TransactionOptions; +import com.mongodb.WriteConcern; +import com.mongodb.internal.async.SingleResultCallback; +import com.mongodb.internal.async.client.AsyncClientSession; +import com.mongodb.internal.operation.AbortTransactionOperation; +import com.mongodb.internal.operation.AsyncReadOperation; +import com.mongodb.internal.operation.AsyncWriteOperation; +import com.mongodb.internal.operation.CommitTransactionOperation; +import com.mongodb.internal.session.BaseClientSessionImpl; +import com.mongodb.internal.session.ServerSessionPool; +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoClient; +import org.reactivestreams.Publisher; +import org.springframework.data.couchbase.transaction.ClientSession; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoSink; + +import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL; +import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL; +import static com.mongodb.assertions.Assertions.assertTrue; +import static com.mongodb.assertions.Assertions.isTrue; +import static com.mongodb.assertions.Assertions.notNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +final class ClientSessionPublisherImpl extends BaseClientSessionImpl implements ClientSession, AsyncClientSession { + + private final OperationExecutor executor; + private TransactionState transactionState = TransactionState.NONE; + private boolean messageSentInCurrentTransaction; + private boolean commitInProgress; + private TransactionOptions transactionOptions; + + ClientSessionPublisherImpl(final ServerSessionPool serverSessionPool, final MongoClient mongoClient, + final ClientSessionOptions options, final OperationExecutor executor) { + super(serverSessionPool, mongoClient, options); + this.executor = executor; + } + + @Override + public boolean hasActiveTransaction() { + return transactionState == TransactionState.IN || (transactionState == TransactionState.COMMITTED && commitInProgress); + } + + @Override + public boolean notifyMessageSent() { + if (hasActiveTransaction()) { + boolean firstMessageInCurrentTransaction = !messageSentInCurrentTransaction; + messageSentInCurrentTransaction = true; + return firstMessageInCurrentTransaction; + } else { + if (transactionState == TransactionState.COMMITTED || transactionState == TransactionState.ABORTED) { + cleanupTransaction(TransactionState.NONE); + } + return false; + } + } + + @Override + public void notifyOperationInitiated(final Object operation) { + assertTrue(operation instanceof AsyncReadOperation || operation instanceof AsyncWriteOperation); + if (!(hasActiveTransaction() || operation instanceof CommitTransactionOperation)) { + assertTrue(getPinnedServerAddress() == null + || (transactionState != TransactionState.ABORTED && transactionState != TransactionState.NONE)); + clearTransactionContext(); + } + } + + @Override + public TransactionOptions getTransactionOptions() { + isTrue("in transaction", transactionState == TransactionState.IN || transactionState == TransactionState.COMMITTED); + return transactionOptions; + } + + @Override + public void startTransaction() { + startTransaction(TransactionOptions.builder().build()); + } + + @Override + public void startTransaction(final TransactionOptions transactionOptions) { + notNull("transactionOptions", transactionOptions); + Boolean snapshot = getOptions().isSnapshot(); + if (snapshot != null && snapshot) { + throw new IllegalArgumentException("Transactions are not supported in snapshot sessions"); + } + if (transactionState == TransactionState.IN) { + throw new IllegalStateException("Transaction already in progress"); + } + if (transactionState == TransactionState.COMMITTED) { + cleanupTransaction(TransactionState.IN); + } else { + transactionState = TransactionState.IN; + } + getServerSession().advanceTransactionNumber(); + this.transactionOptions = TransactionOptions.merge(transactionOptions, getOptions().getDefaultTransactionOptions()); + WriteConcern writeConcern = this.transactionOptions.getWriteConcern(); + if (writeConcern == null) { + throw new MongoInternalException("Invariant violated. Transaction options write concern can not be null"); + } + if (!writeConcern.isAcknowledged()) { + throw new MongoClientException("Transactions do not support unacknowledged write concern"); + } + clearTransactionContext(); + } + + @Override + public void commitTransaction(final SingleResultCallback callback) { + try { + Mono.from(commitTransaction()).subscribe(s -> callback.onResult(s, null), e -> callback.onResult(null, e)); + } catch (Throwable t) { + callback.onResult(null, t); + } + } + + @Override + public void abortTransaction(final SingleResultCallback callback) { + try { + Mono.from(abortTransaction()).subscribe(s -> callback.onResult(s, null), e -> callback.onResult(null, e)); + } catch (Throwable t) { + callback.onResult(null, t); + } + } + + @Override + public AsyncClientSession getWrapped() { + return this; + } + + @Override + public Publisher commitTransaction() { + if (transactionState == TransactionState.ABORTED) { + throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction"); + } + if (transactionState == TransactionState.NONE) { + throw new IllegalStateException("There is no transaction started"); + } + if (!messageSentInCurrentTransaction) { + cleanupTransaction(TransactionState.COMMITTED); + return Mono.create(MonoSink::success); + } else { + ReadConcern readConcern = transactionOptions.getReadConcern(); + if (readConcern == null) { + throw new MongoInternalException("Invariant violated. Transaction options read concern can not be null"); + } + boolean alreadyCommitted = commitInProgress || transactionState == TransactionState.COMMITTED; + commitInProgress = true; + + return executor.execute( + new CommitTransactionOperation(transactionOptions.getWriteConcern(), alreadyCommitted) + .recoveryToken(getRecoveryToken()) + .maxCommitTime(transactionOptions.getMaxCommitTime(MILLISECONDS), MILLISECONDS), + readConcern, this) + .doOnTerminate(() -> { + commitInProgress = false; + transactionState = TransactionState.COMMITTED; + }) + .doOnError(MongoException.class, this::clearTransactionContextOnError); + } + } + + @Override + public Publisher abortTransaction() { + if (transactionState == TransactionState.ABORTED) { + throw new IllegalStateException("Cannot call abortTransaction twice"); + } + if (transactionState == TransactionState.COMMITTED) { + throw new IllegalStateException("Cannot call abortTransaction after calling commitTransaction"); + } + if (transactionState == TransactionState.NONE) { + throw new IllegalStateException("There is no transaction started"); + } + if (!messageSentInCurrentTransaction) { + cleanupTransaction(TransactionState.ABORTED); + return Mono.create(MonoSink::success); + } else { + ReadConcern readConcern = transactionOptions.getReadConcern(); + if (readConcern == null) { + throw new MongoInternalException("Invariant violated. Transaction options read concern can not be null"); + } + return executor.execute( + new AbortTransactionOperation(transactionOptions.getWriteConcern()) + .recoveryToken(getRecoveryToken()), + readConcern, this) + .onErrorResume(Throwable.class, (e) -> Mono.empty()) + .doOnTerminate(() -> { + clearTransactionContext(); + cleanupTransaction(TransactionState.ABORTED); + }); + } + } + + private void clearTransactionContextOnError(final MongoException e) { + if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL) || e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) { + clearTransactionContext(); + } + } + + @Override + public void close() { + if (transactionState == TransactionState.IN) { + Mono.from(abortTransaction()).doOnSuccess(it -> close()).subscribe(); + } else { + super.close(); + } + } + + private void cleanupTransaction(final TransactionState nextState) { + messageSentInCurrentTransaction = false; + transactionOptions = null; + transactionState = nextState; + } + + private enum TransactionState { + NONE, IN, COMMITTED, ABORTED + } +} + diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java b/src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java new file mode 100644 index 000000000..852687800 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java @@ -0,0 +1,10 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package org.springframework.data.couchbase.transaction.internal; + +public interface SingleResultCallback { + void onResult(T var1, Throwable var2); +} diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java index 65aea1e39..c6c9b41d8 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java @@ -19,10 +19,14 @@ import com.couchbase.client.java.query.QueryOptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.transactions.CouchbaseTemplateTransaction2IntegrationTests; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.util.UUID; diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index 1500fa810..a8ba752b1 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -30,14 +30,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.transactions.CouchbaseTemplateTransaction2IntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; import com.couchbase.client.java.query.QueryOptions; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * CouchbaseCache tests Theses tests rely on a cb server running. @@ -45,6 +48,7 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseCacheIntegrationTests extends JavaIntegrationTests { volatile CouchbaseCache cache; @@ -58,9 +62,9 @@ public void beforeEach() { cache = CouchbaseCacheManager.create(couchbaseTemplate.getCouchbaseClientFactory()).createCouchbaseCache("myCache", CouchbaseCacheConfiguration.defaultCacheConfig()); clear(cache); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - cacheManager = ac.getBean(CouchbaseCacheManager.class); - userRepository = ac.getBean(UserRepository.class); + //ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + //cacheManager = ac.getBean(CouchbaseCacheManager.class); + //userRepository = ac.getBean(UserRepository.class); } @AfterEach diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index 2e9f14d1a..1adcd7ff9 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -35,8 +35,10 @@ import java.util.Set; import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; @@ -48,6 +50,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.PersonValue; import org.springframework.data.couchbase.domain.Submission; @@ -56,6 +59,7 @@ import org.springframework.data.couchbase.domain.UserAnnotated2; import org.springframework.data.couchbase.domain.UserAnnotated3; import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.transactions.CouchbaseReactiveTransactionNativeTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; @@ -65,6 +69,7 @@ import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; ; @@ -75,12 +80,15 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { - super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated2.class).all(); diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index 45e3378bb..d8ffd4e58 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -31,18 +31,19 @@ import java.util.UUID; import java.util.stream.Collectors; -import com.couchbase.client.core.msg.kv.DurabilityLevel; 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; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -56,10 +57,12 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.kv.ExistsOptions; import com.couchbase.client.java.kv.GetAnyReplicaOptions; @@ -78,8 +81,12 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + Airport vie = new Airport("airports::vie", "vie", "loww"); @BeforeAll @@ -104,12 +111,16 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); } @AfterEach @@ -801,27 +812,28 @@ void testFluentApi() { RemoveResult rr; result = couchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); - assertEquals(user1,result); + assertEquals(user1, result); result = couchbaseTemplate.upsertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); - assertEquals(user1,result); - result = couchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) - .one(user1); - assertEquals(user1,result); + assertEquals(user1, result); + result = couchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1); + assertEquals(user1, result); rr = couchbaseTemplate.removeById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1.getId()); assertEquals(rr.getId(), user1.getId()); - assertEquals(user1,result); - result = reactiveCouchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) - .one(user1).block(); - assertEquals(user1,result); - result = reactiveCouchbaseTemplate.upsertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) - .one(user1).block(); - assertEquals(user1,result); - result = reactiveCouchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl).one(user1).block(); - assertEquals(user1,result); - rr = reactiveCouchbaseTemplate.removeById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) - .one(user1.getId()).block(); + assertEquals(user1, result); + result = reactiveCouchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1).block(); + assertEquals(user1, result); + result = reactiveCouchbaseTemplate.upsertById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1).block(); + assertEquals(user1, result); + result = reactiveCouchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1).block(); + assertEquals(user1, result); + rr = reactiveCouchbaseTemplate.removeById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1.getId()).block(); assertEquals(rr.getId(), user1.getId()); } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index 3b8f6060a..691f0dc8a 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -33,11 +33,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AssessmentDO; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -53,6 +55,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * Query tests Theses tests rely on a cb server running @@ -63,8 +66,13 @@ * @author Mauro Monti */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests { + @Autowired + public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java index 8fd709ec5..265ad44df 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -37,6 +37,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.core.ReactiveFindByIdOperation.ReactiveFindById; @@ -46,6 +48,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.PersonValue; import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware; import org.springframework.data.couchbase.domain.User; @@ -59,6 +62,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * KV tests Theses tests rely on a cb server running. @@ -67,8 +71,12 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { diff --git a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java index 62b81b1d3..26dafb8a0 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java @@ -37,10 +37,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -53,6 +56,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; @@ -74,10 +78,15 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + @Autowired + public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + Airport vie = new Airport("airports::vie", "vie", "low80"); - ReactiveCouchbaseTemplate template = reactiveCouchbaseTemplate; + ReactiveCouchbaseTemplate template; @BeforeAll public static void beforeAll() { @@ -103,10 +112,14 @@ public void beforeEach() { // then do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); - couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); + + template = reactiveCouchbaseTemplate; } @AfterEach @@ -519,10 +532,10 @@ public void existsByIdOther() { // 1 .one(vie.withIcao("lowg")).block(); try { Boolean exists = template.existsById().inScope(otherScope).inCollection(otherCollection) - .withOptions(existsOptions).one(vie.getId()).block(); - assertTrue(exists, "Airport should exist: " + vie.getId()); + .withOptions(existsOptions).one(saved.getId()).block(); + assertTrue(exists, "Airport should exist: " + saved.getId()); } finally { - template.removeById().inScope(otherScope).inCollection(otherCollection).one(vie.getId()).block(); + template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java new file mode 100644 index 000000000..ae3588c9b --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java @@ -0,0 +1,8 @@ +package org.springframework.data.couchbase.domain; + +public class CollectionsConfig extends Config { + @Override + public String getScopeName(){ + return "my_scope"; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 84d7e6ba9..eb9e44c6c 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -25,6 +25,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; import org.springframework.data.couchbase.cache.CouchbaseCacheConfiguration; import org.springframework.data.couchbase.cache.CouchbaseCacheManager; @@ -60,7 +61,7 @@ @EnableReactiveCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef") @EnableCaching public class Config extends AbstractCouchbaseConfiguration { - String bucketname = "travel-sample"; + String bucketname = "test"; String username = "Administrator"; String password = "password"; String connectionString = "127.0.0.1"; @@ -159,18 +160,16 @@ public void configureRepositoryOperationsMapping(RepositoryOperationsMapping bas // do not use reactiveCouchbaseTemplate for the name of this method, otherwise the value of that bean // will be used instead of the result of this call (the client factory arg is different) - public ReactiveCouchbaseTemplate myReactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { - return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, - new JacksonTranslationService(), getDefaultConsistency()); + public ReactiveCouchbaseTemplate myReactiveCouchbaseTemplate(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return new ReactiveCouchbaseTemplate(reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), getDefaultConsistency()); } // do not use couchbaseTemplate for the name of this method, otherwise the value of that been // will be used instead of the result from this call (the client factory arg is different) - public CouchbaseTemplate myCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + public CouchbaseTemplate myCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter) { - return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), - getDefaultConsistency()); + return new CouchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), getDefaultConsistency()); } // do not use couchbaseClientFactory for the name of this method, otherwise the value of that bean will @@ -231,15 +230,14 @@ public String typeKey() { return "t"; // this will override '_class', is passed in to new CustomMappingCouchbaseConverter } - public static String scopeName = null; - @Override protected String getScopeName() { return scopeName; } - public static void setScopeName(String scopeName) { + static public void setScopeName(String scopeName){ Config.scopeName = scopeName; } + static private String scopeName = null; } diff --git a/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java b/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java index 674f6124f..b16f2810f 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java +++ b/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2021 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. @@ -13,11 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.couchbase.domain; -import static org.junit.jupiter.api.Assertions.assertEquals; - +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryProfile; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.query.QueryScanConsistency; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.util.Pair; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.ParallelFlux; @@ -60,6 +73,8 @@ import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.query.QueryScanConsistency; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * @author Michael Reiche */ @@ -67,8 +82,13 @@ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class FluxTest extends JavaIntegrationTests { - @BeforeAll - public static void beforeEverything() { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + + @BeforeEach + @Override + public void beforeEach() { + /** * The couchbaseTemplate inherited from JavaIntegrationTests uses org.springframework.data.couchbase.domain.Config * It has typeName = 't' (instead of _class). Don't use it. @@ -82,23 +102,19 @@ public static void beforeEverything() { couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection().upsert(k, JsonObject.create().put("x", k)); } + super.beforeEach(); } - @AfterAll - public static void afterEverthing() { + @AfterEach + public void afterEach() { couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + super.afterEach(); for (String k : keyList) { couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection().remove(k); } } - @BeforeEach - @Override - public void beforeEach() { - super.beforeEach(); - } - static List keyList = Arrays.asList("a", "b", "c", "d", "e"); static Collection collection; static ReactiveCollection rCollection; diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index e90a37927..376cdc7e4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -22,15 +22,17 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.core.mapping.Field; import org.springframework.data.couchbase.repository.TransactionResult; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.domain.Persistable; import org.springframework.lang.Nullable; @Document -public class Person extends AbstractEntity { +public class Person extends AbstractEntity implements Persistable { Optional firstname; @Nullable Optional lastname; @@ -49,9 +51,9 @@ public class Person extends AbstractEntity { private Address address; - // Required for use in transactions (??) - //@Transient - //@TransactionResult private TransactionResultHolder txResultHolder; + // Required for use in transactions + @TransactionResult private Integer txResultHolder; + @Transient private boolean isNew; public Person() {} @@ -61,6 +63,7 @@ public Person(String firstname, String lastname) { setFirstname(firstname); setLastname(lastname); setMiddlename("Nick"); + isNew(true); } public Person(int id, String firstname, String lastname) { @@ -166,10 +169,17 @@ public String toString() { public Person withFirstName(String firstName) { Person p = new Person(this.getId(), firstName, this.getLastname()); - //p.txResultHolder = this.txResultHolder; + p.version = version; + p.txResultHolder = this.txResultHolder; return p; } + public Person withVersion(Long version) { + //Person p = new Person(this.getId(), this.getFirstname(), this.getLastname()); + this.version = version; + return this; + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -183,4 +193,13 @@ public boolean equals(Object obj) { return this.getId().equals(that.getId()) && this.getFirstname().equals(that.getFirstname()) && this.getLastname().equals(that.getLastname()) && this.getMiddlename().equals(that.getMiddlename()); } + + @Override + public boolean isNew() { + return isNew; + } + + public void isNew(boolean isNew){ + this.isNew = isNew; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java index c1579f3ef..b6f23ebe2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -18,10 +18,10 @@ import java.util.List; import java.util.UUID; +import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; -import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import com.couchbase.client.java.query.QueryScanConsistency; @@ -29,7 +29,7 @@ /** * @author Michael Reiche */ -public interface PersonRepository extends CrudRepository, DynamicProxyable { +public interface PersonRepository extends CouchbaseRepository, DynamicProxyable { /* * These methods are exercised in HomeController of the test spring-boot DemoApplication @@ -96,7 +96,7 @@ public interface PersonRepository extends CrudRepository, Dynami boolean existsById(UUID var1); - Iterable findAll(); + List findAll(); long count(); diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 7143bb269..b946d0ab8 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -32,6 +32,9 @@ import java.util.Optional; import java.util.UUID; +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.java.env.ClusterEnvironment; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -221,6 +224,15 @@ public String getBucketName() { return bucketName(); } + @Override + protected void configureEnvironment(ClusterEnvironment.Builder builder) { + if(getConnectionString().contains("cloud.couchbase.com")) { + builder.securityConfig(SecurityConfig.builder() + .trustManagerFactory(InsecureTrustManagerFactory.INSTANCE) + .enableTls(true)); + } + } + } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index c085451de..d06f879ef 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -381,7 +381,7 @@ public void saveRequestPlusWithDefaultRepository() { Airport vie = new Airport("airports::vie", "vie", "low9"); Airport saved = airportRepositoryRP.save(vie); - List allSaved = airportRepositoryRP.findAll(); + List allSaved = airportRepositoryRP.findAll(REQUEST_PLUS); couchbaseTemplate.removeById(Airport.class).one(saved.getId()); assertEquals(1, allSaved.size(), "should have found 1 airport"); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index b720ae59c..44e51ecce 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -32,11 +32,14 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.AddressAnnotated; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportRepository; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserCol; @@ -49,6 +52,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexFailureException; import com.couchbase.client.core.io.CollectionIdentifier; @@ -57,12 +61,16 @@ import com.couchbase.client.java.query.QueryScanConsistency; @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { - @Autowired AirportRepository airportRepository; // initialized in beforeEach() - @Autowired UserColRepository userColRepository; // initialized in beforeEach() - @Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; // initialized in beforeEach() - @Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; // initialized in beforeEach() + @Autowired AirportRepository airportRepository; + @Autowired UserColRepository userColRepository; + @Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; + @Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; + + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @BeforeAll public static void beforeAll() { @@ -87,14 +95,10 @@ public void beforeEach() { // then do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); couchbaseTemplate.removeByQuery(UserCol.class).inScope(otherScope).inCollection(otherCollection).all(); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); // seems that @Autowired is not adequate, so ... - airportRepository = (AirportRepository) ac.getBean("airportRepository"); - userColRepository = (UserColRepository) ac.getBean("userColRepository"); - userSubmissionAnnotatedRepository = (UserSubmissionAnnotatedRepository) ac - .getBean("userSubmissionAnnotatedRepository"); - userSubmissionUnannotatedRepository = (UserSubmissionUnannotatedRepository) ac - .getBean("userSubmissionUnannotatedRepository"); + // airportRepository = (AirportRepository) ac.getBean("airportRepository"); + // userColRepository = (UserColRepository) ac.getBean("userColRepository"); } @AfterEach @@ -228,7 +232,7 @@ public void testScopeCollectionAnnotationSwap() { userColRepository.delete(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); } finally { - couchbaseTemplate.removeByQuery(Airport.class).all(); + List removed = couchbaseTemplate.removeByQuery(Airport.class).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java index 51f6a5efe..0318d4634 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -29,7 +29,10 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.ReactiveAirportRepository; import org.springframework.data.couchbase.domain.ReactiveUserColRepository; @@ -39,6 +42,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexFailureException; import com.couchbase.client.core.io.CollectionIdentifier; @@ -47,10 +51,13 @@ import com.couchbase.client.java.query.QueryScanConsistency; @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired ReactiveAirportRepository airportRepository; @Autowired ReactiveUserColRepository userColRepository; + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @BeforeAll public static void beforeAll() { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java new file mode 100644 index 000000000..1771e909e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java @@ -0,0 +1,33 @@ +package org.springframework.data.couchbase.transactions; + +import lombok.Data; + +import org.springframework.data.domain.Persistable; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ +@Data +public class AfterTransactionAssertion { + + private final T persistable; + private boolean expectToBePresent; + + public void isPresent() { + expectToBePresent = true; + } + + public void isNotPresent() { + expectToBePresent = false; + } + + public Object getId() { + return persistable.getId(); + } + + public boolean shouldBePresent() { + return expectToBePresent; + } +} + diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java new file mode 100644 index 000000000..9c1f5dda6 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -0,0 +1,1088 @@ +/* + * Copyright 2012-2021 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import lombok.Data; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +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.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.TransactionsWrapper; +import org.springframework.data.couchbase.util.Capabilities; +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.ReactiveTransaction; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.Transactions; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; +import com.couchbase.transactions.error.TransactionFailed; +import com.couchbase.transactions.error.external.TransactionOperationFailed; + +/** + * Tests for com.couchbase.transactions using + *
  • couchbase reactive transaction manager via transactional operator couchbase non-reactive transaction + * manager via @Transactional @Transactional(transactionManager = + * BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER)
  • + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbasePersonTransactionIntegrationTests.Config.class) +public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + @Autowired CouchbaseTransactionManager couchbaseTransactionManager; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired CouchbaseTemplate cbTmpl; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + @Autowired Transactions transactions; + /* DO NOT @Autowired - it will result in no @Transactional annotation behavior */ PersonService personService; + @Autowired CouchbaseTemplate operations; + + static GenericApplicationContext context; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionIntegrationTests.Config.class, + PersonService.class); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + context.close(); + } + + @BeforeEach + public void beforeEachTest() { + personService = context.getBean(PersonService.class); // getting it via autowired results in no @Transactional + // Skip this as we just one to track TransactionContext + operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); // doesn't work??? + operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List e = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + + Person walterWhite = new Person(1, "Walter", "White"); + try { + couchbaseClientFactory.getBucket().defaultCollection().remove(walterWhite.getId().toString()); + } catch (Exception ex) { + //System.err.println(ex); + } + } + + /* + List>> assertionList; + + @BeforeTransaction + public void beforeTransaction() { + System.err.println("BeforeTransaction"); + assertionList = new ArrayList<>(); + } + + @AfterTransaction + public void afterTransaction() { + System.err.println("AfterTransaction"); + if (assertionList == null) { + return; + } + assertionList.forEach(it -> { + Person p = (Person) (operations.findById(it.getPersistable().getClass()).one(it.getId().toString())); + boolean isPresent = p != null; + System.err.println(("isPresent: " + isPresent + " shouldBePresent: " + it.shouldBePresent())); + assertThat(isPresent).isEqualTo(it.shouldBePresent()) + .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), + it.shouldBePresent() ? "be present" : "NOT be present")); + }); + } + + private AfterTransactionAssertion assertAfterTransaction(Person p) { + AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(p); + if (assertionList != null) { + assertionList.add(assertion); + } + return assertion; + } + */ + + @Test + public void shouldRollbackAfterException() { + Person p = new Person(null, "Walter", "White"); + assertThrows(SimulateFailureException.class, () -> personService.savePersonErrors(p)); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { + Person p = new Person(null, "Walter", "White"); + assertThrows(SimulateFailureException.class, () -> personService.declarativeSavePersonErrors(p)); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void shouldRollbackAfterExceptionOfTxAnnotatedMethodReactive() { + Person p = new Person(null, "Walter", "White"); + assertThrows(SimulateFailureException.class, () -> personService.declarativeSavePersonErrorsReactive(p).block()); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void commitShouldPersistTxEntries() { + Person p = new Person(null, "Walter", "White"); + Person s = personService.savePerson(p); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { + Person p = new Person(null, "Walter", "White"); + Person s = personService.declarativeSavePerson(p); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void commitShouldPersistTxEntriesOfTxAnnotatedMethodReactive() { + Person p = new Person(null, "Walter", "White"); + Person s = personService.declarativeSavePersonReactive(p).block(); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void commitShouldPersistTxEntriesAcrossCollections() { + List persons = personService.saveWithLogs(new Person(null, "Walter", "White")); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + Long countEvents = operations.count(new Query(), EventLog.class); // + assertEquals(4, countEvents, "should have saved and found 4"); + } + + @Test + public void rollbackShouldAbortAcrossCollections() { + assertThrows(SimulateFailureException.class, + () -> personService.saveWithErrorLogs(new Person(null, "Walter", "White"))); + List persons = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + assertEquals(0, persons.size(), "should have done roll back and left 0 entries"); + List events = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); // + assertEquals(0, events.size(), "should have done roll back and left 0 entries"); + } + + @Test + public void countShouldWorkInsideTransaction() { + Long count = personService.countDuringTx(new Person(null, "Walter", "White")); + assertEquals(1, count, "should have counted 1 during tx"); + } + + @Test + public void emitMultipleElementsDuringTransaction() { + List docs = personService.saveWithLogs(new Person(null, "Walter", "White")); + assertEquals(4, docs.size(), "should have found 4 eventlogs"); + } + + @Test + public void errorAfterTxShouldNotAffectPreviousStep() { + Person p = personService.savePerson(new Person(null, "Walter", "White")); + assertThrows(TransactionOperationFailed.class, () -> personService.savePerson(p)); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + /** + * This will appear to work even if replaceById does not use a transaction. + */ + @Test + @Disabled + public void replacePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + cbTmpl.insertById(Person.class).one(person); + Mono result = transactions.reactive(ctx -> { // get the ctx + ClientSession clientSession = couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap((pp) -> { + System.err.println("==================================== ATTEMPT : " + ctx.attemptId() + + " ======================================"); + return Mono.just(pp); + }) // + .flatMap((pp) -> rxCBTmpl.replaceById(Person.class).one(pp)) // + .then(Mono.just(synchronizationManager)); // tx + }); + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); + }); + + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertEquals(person, pFound, "should have found expected"); + } + + @Test + public void insertPersonCBTransactionsRxTmplRollback() { + Person person = new Person(1, "Walter", "White"); + try { + rxCBTmpl.removeById(Person.class).one(person.getId().toString()); + } catch(DocumentNotFoundException dnfe){} + Mono result = transactions.reactive(ctx -> { // get the ctx + + ClientSession clientSession = couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + // execute the transaction (insertById, SimulateFailure), insertById() will fetch the ctx from the context + return rxCBTmpl.insertById(Person.class).one(person).then(Mono.error(new SimulateFailureException())); // tx + }); + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); + + }); + assertThrowsCause(TransactionFailed.class, SimulateFailureException.class, (ignore) -> { + result.block(); + return null; + }); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertNull(pFound, "insert should have been rolled back"); + } + + @Test + public void insertTwicePersonCBTransactionsRxTmplRollback() { + Person person = new Person(1, "Walter", "White"); + sleepMs(1000); + Mono result = transactions.reactive(ctx -> { // get the ctx + ClientSession clientSession = couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + return rxCBTmpl.insertById(Person.class).one(person) // + .flatMap((ppp) -> rxCBTmpl.insertById(Person.class).one(ppp)) // + .then(Mono.just(synchronizationManager)); // tx + }); + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); + }); + assertThrowsCause(TransactionFailed.class, DuplicateKeyException.class, (ignore) -> { + result.block(); + return null; + }); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertNull(pFound, "insert should have been rolled back"); + } + + @Test + public void replaceWithCasConflictResolvedViaRetry() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + cbTmpl.insertById(Person.class).one(person); + + AtomicInteger tryCount = new AtomicInteger(0); + Mono result = transactions.reactive(ctx -> { // get the ctx + // see TransactionalOperatorImpl.tranactional(). + ClientSession clientSession = couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap((ppp) -> { + tryCount.getAndIncrement(); + System.err.println("===== ATTEMPT : " + tryCount.get() + " " + ctx.attemptId() + " ====="); + return Mono.just(ppp); + })// + .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)) // + .then(Mono.just(synchronizationManager)); // tx + }); + + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); + }); + + result.block(); + + stop.set(true); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertTrue(tryCount.get() > 1, "should have been more than one try "); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + @Test + public void wrapperReplaceWithCasConflictResolvedViaRetry() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + cbTmpl.insertById(Person.class).one(person); + + AtomicInteger tryCount = new AtomicInteger(0); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(transactions, + reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.reactive(ctx -> { + tryCount.incrementAndGet(); + return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)).then(); + }); + TransactionResult txResult = result.block(); + stop.set(true); + System.out.println("txResult: "+txResult); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + /** + * This does process retries - by + * CallbackTransactionManager.execute() -> transactions.run() -> executeTransaction() -> retryWhen. + * + * The CallbackTransactionManager only finds the resources in the Thread - it doesn't find it in the context. + * + * It might be nice to use the context for both - but I'm not sure if that is possible - mostly due + * to ExecutableFindById.one() calling reactive.one().block() instead of returning a publisher which could + * have .contextWrite() called on it. + */ + @Test + public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + cbTmpl.insertById(Person.class).one(person); + AtomicInteger tryCount = new AtomicInteger(0); + Person p = personService.declarativeFindReplacePersonCallback(person, tryCount); + stop.set(true); + System.out.println("person: "+p); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + /** + * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. + * I don't know why it isn't retried. This seems like it is due to the functioning of + * AbstractReactiveTransactionManager + */ + @Test + public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + cbTmpl.insertById(Person.class).one(person); + AtomicInteger tryCount = new AtomicInteger(0); + Person p = personService.declarativeFindReplacePersonReactive(person, tryCount).block(); + stop.set(true); + System.out.println("person: "+p); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + @Test + /** + * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. + * I don't know why it isn't retried. This seems like it is due to the functioning of + * AbstractPlatformTransactionManager + */ + public void replaceWithCasConflictResolvedViaRetryAnnotated() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + cbTmpl.insertById(Person.class).one(person); + AtomicInteger tryCount = new AtomicInteger(0); + Person p = personService.declarativeFindReplacePerson(person, tryCount); + stop.set(true); + System.out.println("person: "+p); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + private class ReplaceLoopThread extends Thread { + AtomicBoolean stop; + Person person; + + public ReplaceLoopThread(AtomicBoolean stop, Person person) { + this.stop = stop; + this.person = person; + } + + public void run() { + for (int i = 0; i < 100 && !stop.get(); i++) { + sleepMs(5); + try { + // note that this does not go through spring-data, therefore it does not have the @Field , @Version etc. + // annotations processed so we just check getFirstname().equals() + // switchedPerson has version=0, so it doesn't check CAS + couchbaseClientFactory.getBucket().defaultCollection().replace(person.getId().toString(), person); + System.out.println("********** replace thread: " + i + " success"); + } catch (Exception e) { + System.out.println("********** replace thread: " + i + " " + e.getClass().getName()); + } + } + + } + } + /* + @Test + public void replacePersonCBTransactionsRxTmplRollback() { + Person person = new Person(1, "Walter", "White"); + String newName = "Walt"; + rxCBTmpl.insertById(Person.class).one(person).block(); + sleepMs(1000); + Mono result = transactions.reactive(((ctx) -> { // get the ctx + // can we take the AttemptContextReactive ctx and save it in the context? + ClientSession clientSession = couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(clientSession, couchbaseClientFactory); + + // I think this needs to happen within the transactions.reactive() call - or equivalent. + + // this currentContext() call is going to create a new ctx, and store the acr. Will it get uses in syncFlatMap() + // below? Should the ctx be created in the above call to transactions.reactive()? + // How does this work in savePerson etc? + // is there means for just getting the currentContext() without creating it? + Mono sync = TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new) + .flatMap(synchronizationManager -> { + synchronizationManager.bindResource(couchbaseClientFactory, resourceHolder); // is this binding to the right syncManager? + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + return Mono.just(synchronizationManager); + }).contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + + + return sync.flatMap( (ignore) -> { + System.out.println("TSM: "+ignore); + return rxCBTmpl.findById(Person.class) + .one(person.getId().toString()); }) // need to get the TSM context in the one() calls. + .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName("Walt"))).then(Mono.empty())); + })); + + + result.block(); + // assertThrows(TransactionFailed.class, () -> result.block()); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + System.err.println(pFound); + assertEquals(person.getFirstname(), pFound.getFirstname()); + } + + */ + + /* + @Test + public void deletePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + rxCBTmpl.insertById(Person.class).one(person).block(); + sleepMs(1000); + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).transaction(new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager)).one(person.getId().toString()) + .then(); + })); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(); + })); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxTmplFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) + .then(); + })); + assertThrows(TransactionFailed.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// + + @Test + public void deletePersonCBTransactionsRxRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); + })); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxRepoFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) + .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); + })); + assertThrows(TransactionFailed.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void findPersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + })); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + @Test + // @Transactional + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void insertPersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailed e) { + e.printStackTrace(); + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void replacePersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) + .one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void findPersonSpringTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + }); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + */ + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, + ReactiveTransaction status, TransactionDefinition definition) { + + // if (status.isNewSynchronization()) { + synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); + synchronizationManager.setCurrentTransactionIsolationLevel( + definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() + : null); + synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); + synchronizationManager.setCurrentTransactionName(definition.getName()); + synchronizationManager.initSynchronization(); + // } + } + + void assertThrowsCause(Class exceptionClass, Class causeClass, Function function) { + try { + function.apply(null); + } catch (Throwable tfe) { + System.err.println("Exception: " + tfe + " causedBy: " + tfe.getCause()); + if (tfe.getClass().isAssignableFrom(exceptionClass)) { + if (tfe.getCause() != null && tfe.getCause().getClass().isAssignableFrom(causeClass)) { + System.err.println("thrown exception was: "+tfe+" cause: "+tfe.getCause()); + return; + } + } + throw new RuntimeException("expected " + exceptionClass + " with cause " + causeClass + " but got " + tfe); + } + throw new RuntimeException("expected " + exceptionClass + " with cause " + causeClass + " nothing was thrown"); + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Override + public TransactionConfig transactionConfig() { + // expirationTime 20 minutes for stepping with the debugger + return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(20)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); + } + + + /* + beforeAll creates a PersonService bean in the applicationContext + + context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionIntegrationTests.Config.class, + PersonService.class); + + @Bean("personService") + PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + return new PersonService(ops, mgr, opsRx, mgrRx); + } + */ + + } + + @Data + // @AllArgsConstructor + static class EventLog { + public EventLog() {} + + ; + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + public EventLog(String id, String action) { + this.id = id; + this.action = action; + } + + String id; + String action; + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("EventLog : {\n"); + sb.append(" id : " + getId()); + sb.append(", action: " + action); + return sb.toString(); + } + } + + @Service + @Component + @EnableTransactionManagement + static + // @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + class PersonService { + + final CouchbaseOperations personOperations; + final CouchbaseTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; + final ReactiveCouchbaseOperations personOperationsRx; + final ReactiveCouchbaseTransactionManager managerRx; + + public PersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, + ReactiveCouchbaseTransactionManager mgrRx) { + personOperations = ops; + manager = mgr; + System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); + System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); + System.err.println("manager Manager : " + manager); + + personOperationsRx = opsRx; + managerRx = mgrRx; + System.out + .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); + System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); + System.out.println("managerRx Manager : " + managerRx); + return; + } + + public Person savePersonErrors(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class).one(person)// + . flatMap(it -> Mono.error(new SimulateFailureException()))// + .as(transactionalOperator::transactional).block(); + } + + public Person savePerson(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + return personOperationsRx.insertById(Person.class).one(person)// + .as(transactionalOperator::transactional).block(); + } + + public Long countDuringTx(Person person) { + assertInAnnotationTransaction(false); + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class).one(person)// + .then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) + .as(transactionalOperator::transactional).block(); + } + + // @Transactional + public List saveWithLogs(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux + .merge(personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.insertById(Person.class).one(person), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // + .as(transactionalOperator::transactional).collectList().block(); + + } + + public List saveWithErrorLogs(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux + .merge(personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.insertById(Person.class).one(person), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // + . flatMap(it -> Mono.error(new SimulateFailureException())).as(transactionalOperator::transactional) + .collectList().block(); + + } + + // org.springframework.beans.factory.NoUniqueBeanDefinitionException: + // No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single + // matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + return personOperations.insertById(Person.class).one(person); + } + + public Person savePersonBlocking(Person person) { + if (1 == 1) + throw new RuntimeException("no implemented"); + assertInAnnotationTransaction(true); + return personOperations.insertById(Person.class).one(person); + + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeSavePersonErrors(Person person) { + assertInAnnotationTransaction(true); + Person p = personOperations.insertById(Person.class).one(person); // + SimulateFailureException.throwEx(); + return p; + } + + @Autowired + CouchbaseCallbackTransactionManager callbackTm; + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: "+tryCount.incrementAndGet()); + System.err.println("declarativeFindReplacePersonCallback cluster : "+callbackTm.template().getCouchbaseClientFactory().getCluster().block()); + System.err.println("declarativeFindReplacePersonCallback resourceHolder : "+org.springframework.transaction.support.TransactionSynchronizationManager.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); + /* what are we trying to see here??? + TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { + System.err.println("declarativeFindReplacePersonCallback reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); + return Mono.just(sm); + }).block(); + */ + Person p = personOperations.findById(Person.class).one(person.getId().toString()); + return personOperations.replaceById(Person.class).one(p); + } + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonReactive try: "+tryCount.incrementAndGet()); + /* NoTransactionInContextException + TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { + System.err.println("declarativeFindReplacePersonReactive reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); + return Mono.just(sm); + }).block(); + */ + return personOperationsRx.findById(Person.class).one(person.getId().toString()) + .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p)); + } + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePerson try: "+tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).one(person.getId().toString()); + return personOperations.replaceById(Person.class).one(p); + } + + + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + public Mono declarativeSavePersonReactive(Person person) { + assertInAnnotationTransaction(true); + return personOperationsRx.insertById(Person.class).one(person); + } + + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + public Mono declarativeSavePersonErrorsReactive(Person person) { + assertInAnnotationTransaction(true); + Mono p = personOperationsRx.insertById(Person.class).one(person); // + SimulateFailureException.throwEx(); + return p; + } + + void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName().startsWith("org.springframework.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException( + "in transaction = " + (!inTransaction) + " but expected in annotation transaction = " + inTransaction); + } + + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java new file mode 100644 index 000000000..9da5bbb73 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -0,0 +1,617 @@ +/* + * Copyright 2012-2021 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.Cluster; +import lombok.Data; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.domain.Persistable; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbasePersonTransactionReactiveIntegrationTests.Config.class) +//@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + @Autowired CouchbaseTransactionManager couchbaseTransactionManager; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + + @Autowired Cluster myCluster; + + /* DO NOT @Autowired */ PersonService personService; + + static GenericApplicationContext context; + @Autowired ReactiveCouchbaseTemplate operations; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionReactiveIntegrationTests.Config.class, + CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + personService = context.getBean(CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); // getting it via autowired results in no @Transactional + operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + } + + + @Test // DATAMONGO-2265 + public void shouldRollbackAfterException() { + personService.savePersonErrors(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .verifyError(RuntimeException.class); + // operations.findByQuery(Person.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); + // sleepMs(5000); + operations.count(new Query(), Person.class) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + // @Rollback(false) + public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { + Person p = new Person(null, "Walter", "White"); + try { + personService.declarativeSavePersonErrors(p) // + .as(StepVerifier::create) // + .expectComplete(); + // .verifyError(RuntimeException.class); + } catch (RuntimeException e) { + if (e instanceof SimulateFailureException || (e.getMessage() != null && e.getMessage().contains("poof"))) { + System.err.println(e); + } else { + throw e; + } + } + + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntries() { + + personService.savePerson(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count().block(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { + + personService.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntriesAcrossCollections() { + + personService.saveWithLogs(new Person(null, "Walter", "White")) // + .then() // + .as(StepVerifier::create) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(4L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void rollbackShouldAbortAcrossCollections() { + + personService.saveWithErrorLogs(new Person(null, "Walter", "White")) // + .then() // + .as(StepVerifier::create) // + .verifyError(); + + operations.count(new Query(), Person.class) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + + operations.count(new Query(), EventLog.class)// + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void countShouldWorkInsideTransaction() { + + personService.countDuringTx(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void emitMultipleElementsDuringTransaction() { + + try { + personService.saveWithLogs(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNextCount(4L) // + .verifyComplete(); + } catch (Exception e) { + System.err.println("Done"); + throw e; + } + } + + @Test // DATAMONGO-2265 + public void errorAfterTxShouldNotAffectPreviousStep() { + + Person p = new Person(1, "Walter", "White"); + remove(couchbaseTemplate, "_default", p.getId().toString()); + personService.savePerson(p) // + //.delayElement(Duration.ofMillis(100)) // + .then(Mono.error(new RuntimeException("my big bad evil error"))).as(StepVerifier::create) // + .expectError() + .verify(); + //.expectError() // + //.as(StepVerifier::create) + //.expectNext(p) + //.verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + // @RequiredArgsConstructor + static class PersonService { + + final ReactiveCouchbaseOperations personOperationsRx; + final ReactiveCouchbaseTransactionManager managerRx; + final CouchbaseOperations personOperations; + final CouchbaseTransactionManager manager; + + public PersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, + ReactiveCouchbaseTransactionManager mgrRx) { + personOperations = ops; + manager = mgr; + System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); + System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); + System.err.println("manager Manager : " + manager); + + personOperationsRx = opsRx; + managerRx = mgrRx; + System.out + .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); + System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); + System.out.println("managerRx Manager : " + managerRx); + return; + } + + public Mono savePersonErrors(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx); + return personOperationsRx.insertById(Person.class).one(person) // + . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // + .as(transactionalOperator::transactional); + } + + public Mono savePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class).one(person) // + .flatMap(Mono::just) // + .as(transactionalOperator::transactional); + } + + public Mono countDuringTx(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.save(person) // + .then(personOperationsRx.count(new Query(), Person.class)) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithLogs(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId().toString(), "beforeConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.save(person), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithErrorLogs(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId(), "beforeConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.save(person), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // + . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // + .as(transactionalOperator::transactional); + } + + @Transactional + public Flux declarativeSavePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Flux declarativeSavePersonErrors(Person person) { + Person p = personOperations.insertById(Person.class).one(person); + // if(1==1)throw new RuntimeException("poof!"); + Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0); + System.err.println("pp=" + pp); + SimulateFailureException.throwEx(); + return Flux.just(p); + } + } + + /* + @Test + public void deletePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(); + })); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxTmplFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) + .then(); + })); + assertThrows(TransactionFailed.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// + + @Test + public void deletePersonCBTransactionsRxRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); + })); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxRepoFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) + .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); + })); + assertThrows(TransactionFailed.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void findPersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + })); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void insertPersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailed e) { + e.printStackTrace(); + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void replacePersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) + .one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void findPersonSpringTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + }); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + */ + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Override + public TransactionConfig transactionConfig() { + return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); + } + + @Bean + public Cluster couchbaseCluster() { + return Cluster.connect("10.144.220.101", "Administrator", "password"); + } + + /* + @Bean("personService") + PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + return new PersonService(ops, mgr, opsRx, mgrRx); + } + */ + + } + + @Data + // @AllArgsConstructor + static class EventLog { + public EventLog() {} + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + public EventLog(String id, String action) { + this.id = id; + this.action = action; + } + + String id; + String action; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java new file mode 100644 index 000000000..7ca0847d1 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -0,0 +1,439 @@ +/* + * Copyright 2012-2021 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Disabled; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.reactive.TransactionalOperator; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; +import com.couchbase.transactions.error.TransactionFailed; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbaseReactiveTransactionNativeTests.Config.class) +@Disabled // Now using TransactionSyncronizationManager for the session +public class CouchbaseReactiveTransactionNativeTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + + static String cName; // short name + + static GenericApplicationContext context; + ReactiveCouchbaseTemplate operations; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + cName = null;// cName; + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + context.close(); + } + + @BeforeEach + public void beforeEachTest() { + operations = rxCBTmpl; + + } + + @Test + public void replacePersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void replacePersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); +sleepMs(1000); + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + // Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> ctx.template(rxCBTmpl).insertById(Person.class) + .one(person).flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> ctx.template(rxCBTmpl).insertById(Person.class) + .one(person).flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + // Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void replacePersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(rxRepo).withCollection(cName).findById(person.getId().toString()) + .flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + // Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(rxRepo).withTransaction(txOperator).withCollection(cName).save(person) // insert + //.flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) // replace + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + // Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonRepo() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(rxRepo).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) // replace + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + /* + @Test + public void replacePersonRbSpringTransactional() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + + TransactionalOperator txOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> { + ctx.transactionResultHolder(123); + return ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .as(txOperator::transactional).then(); + }, false); + + //TransactionalOperator txOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager, new CouchbaseTransactionDefinition()); + //Mono result = txOperator.reactive((ctx) -> { + // ctx.transactionResultHolder(123); + // return ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + // .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + // .as(txOperator::transactional).then(); + //}, false); + + try { + result.block(); + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } +*/ + @Test + public void findReplacePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> { + rxCBTmpl.support().getTxResultHolder(person); + return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(txOperator).one(person.getId().toString()) + .flatMap(pGet -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(txOperator) + .one(pGet.withFirstName("Walt"))) + .then(); + }); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt"); + } + + @Test + public void insertReplacePersonsCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> { + return rxCBTmpl + .insertById(Person.class).inCollection(cName).transaction(txOperator).one(person).flatMap(pInsert -> rxCBTmpl + .replaceById(Person.class).inCollection(cName).transaction(txOperator).one(pInsert.withFirstName("Walt"))) + .then(); + }); + + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt"); + } + + @Test void transactionalSavePerson(){ + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + savePerson(person).block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found "+person); + } + + public Mono savePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager); + + return operations.save(person) // + .flatMap(Mono::just) // + .as(transactionalOperator::transactional); + } + + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Override + public TransactionConfig transactionConfig() { + return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); + } + + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java new file mode 100644 index 000000000..547959cec --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java @@ -0,0 +1,211 @@ +/* + * Copyright 2018-2021 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.assertj.core.api.Assertions.assertThat; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.data.domain.Persistable; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; +import com.example.demo.CouchbaseTransactionManager; +import com.example.demo.CouchbaseTransactionalTemplate; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ + +@ContextConfiguration +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +@SpringJUnitConfig(CouchbaseTemplateTransaction2IntegrationTests.Config.class) +public class CouchbaseTemplateTransaction2IntegrationTests extends JavaIntegrationTests { + + static final String DB_NAME = "template-tx-tests"; + static final String COLLECTION_NAME = "assassins"; + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Override + public TransactionConfig transactionConfig() { + return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.NONE).build(); + } + + @Bean + public CouchbaseTransactionManager transactionManager(@Autowired CouchbaseTemplate template, + @Autowired TransactionConfig transactionConfig) { + return new CouchbaseTransactionManager(template, transactionConfig); + } + + @Bean + public CouchbaseTransactionalTemplate transactionalTemplate(CouchbaseTransactionManager manager) { + return manager.template(); + } + + } + + @Autowired CouchbaseTemplate template; + + List>> assertionList; + + @BeforeEach + public void setUp() { + + // template.setReadPreference(ReadPreference.primary()); + assertionList = new CopyOnWriteArrayList<>(); + } + + @BeforeTransaction + public void beforeTransaction() { + template.removeByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + Collection a = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + } + + @AfterTransaction + public void verifyDbState() { + + // Collection collection = template.getCollection("_default") ;// + // client.getDatabase(DB_NAME).getCollection(COLLECTION_NAME); + + Collection p = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + System.out.println("assassins: " + p); + assertionList.forEach(it -> { + + boolean isPresent = template.findById(Assassin.class).one(it.getId().toString()) != null; // (Filters.eq("_id", + // it.getId())) != 0; + + assertThat(isPresent).isEqualTo(it.shouldBePresent()) + .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), + it.shouldBePresent() ? "be present" : "NOT be present")); + }); + } + + @Rollback(false) + @Transactional() + @Test // DATAMONGO-1920 + public void shouldOperateCommitCorrectly() { + + Assassin hu = new Assassin("hu", "Hu Gibbet"); + template.insertById(Assassin.class).one(hu); + + assertAfterTransaction(hu).isPresent(); + } + + @Test // DATAMONGO-1920 + // @Rollback(false) by default on these tests + public void shouldOperateRollbackCorrectly() { + + Assassin vi = new Assassin("vi", "Viridiana Sovari"); + try { + template.removeById(Assassin.class).one(vi.getId()); // could be something that is not an Assassin + } catch (DataRetrievalFailureException dnfe) {} + template.insertById(Assassin.class).one(vi); + assertAfterTransaction(vi).isNotPresent(); + } + + @Test // DATAMONGO-1920 + // @Rollback(false) by default on these tests + public void shouldBeAbleToViewChangesDuringTransaction() throws InterruptedException { + Assassin durzo = new Assassin("durzo", "Durzo Blint"); + template.insertById(Assassin.class).one(durzo); + Assassin retrieved = template.findById(Assassin.class).one(durzo.getId()); + assertThat(retrieved).isEqualTo(durzo); + assertAfterTransaction(durzo).isNotPresent(); + } + + // --- Just some helpers and tests entities + + private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) { + + AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(assassin); + assertionList.add(assertion); + return assertion; + } + + @Data + @AllArgsConstructor + @Document + static class Assassin implements Persistable { + + @Id String id; + String name; + + @Override + public boolean isNew() { + return id == null; + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java new file mode 100644 index 000000000..32786ee67 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2018-2021 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.assertj.core.api.Assertions.assertThat; + +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.env.ClusterEnvironment; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.data.domain.Persistable; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ +// @ContextConfiguration - not needed?? +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +@SpringJUnitConfig(CouchbaseTemplateTransactionIntegrationTests.Config.class) +public class CouchbaseTemplateTransactionIntegrationTests extends JavaIntegrationTests { + + static final String DB_NAME = "template-tx-tests"; + static final String COLLECTION_NAME = "assassins"; + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Override + public TransactionConfig transactionConfig() { + return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.NONE).build(); + } + + } + + @Autowired CouchbaseTemplate template; + + List>> assertionList; + + @BeforeEach + public void setUp() { + // template.setReadPreference(ReadPreference.primary()); + assertionList = new CopyOnWriteArrayList<>(); + } + + @BeforeTransaction + public void beforeTransaction() { + template.removeByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + Collection a = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + System.err.println("assassins before tx: " + a); + } + + @AfterTransaction + public void verifyDbState() { + + // Collection collection = template.getCollection("_default") ;// + // client.getDatabase(DB_NAME).getCollection(COLLECTION_NAME); + + Collection p = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + System.out.println("assassins: " + p); + assertionList.forEach(it -> { + + boolean isPresent = template.findById(Assassin.class).one(it.getId().toString()) != null; // (Filters.eq("_id", + // it.getId())) != 0; + + assertThat(isPresent).isEqualTo(it.shouldBePresent()) + .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), + it.shouldBePresent() ? "be present" : "NOT be present")); + }); + } + + @Rollback(false) + @Test // DATAMONGO-1920 + public void shouldOperateCommitCorrectly() { + Assassin hu = new Assassin("hu", "Hu Gibbet"); + template.insertById(Assassin.class).one(hu); + assertAfterTransaction(hu).isPresent(); + } + + @Test // DATAMONGO-1920 + // @Rollback(false) by default on these tests + public void shouldOperateRollbackCorrectly() { + Assassin vi = new Assassin("vi", "Viridiana Sovari"); + try { + template.removeById(Assassin.class).one(vi.getId()); // could be something that is not an Assassin + } catch (DataRetrievalFailureException dnfe) {} + template.insertById(Assassin.class).one(vi); + assertAfterTransaction(vi).isNotPresent(); + } + + @Test // DATAMONGO-1920 + // @Rollback(false) by default on these tests + public void shouldBeAbleToViewChangesDuringTransaction() throws InterruptedException { + Assassin durzo = new Assassin("durzo", "Durzo Blint"); + template.insertById(Assassin.class).one(durzo); + Assassin retrieved = template.findById(Assassin.class).one(durzo.getId()); + assertThat(retrieved).isEqualTo(durzo); + assertAfterTransaction(durzo).isNotPresent(); + } + + // --- Just some helpers and tests entities + + private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) { + + AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(assassin); + assertionList.add(assertion); + return assertion; + } + + @Data + @AllArgsConstructor + @Document + static class Assassin implements Persistable { + + @Id String id; + String name; + + @Override + public boolean isNew() { + return id == null; + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java deleted file mode 100644 index 134a9ce9a..000000000 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright 2012-2021 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 static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; -import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TRANSACTIONS; -import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; - -import org.junit.jupiter.api.Disabled; -import reactor.core.publisher.Mono; - -import java.io.IOException; -import java.time.Duration; -import java.util.LinkedList; -import java.util.List; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.dao.DataRetrievalFailureException; -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.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.core.query.QueryCriteria; -import org.springframework.data.couchbase.domain.Config; -import org.springframework.data.couchbase.domain.Person; -import org.springframework.data.couchbase.domain.ReactivePersonRepository; -import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterType; -import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; -import org.springframework.data.couchbase.util.IgnoreWhen; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import com.couchbase.client.core.error.DocumentNotFoundException; -import com.couchbase.client.java.Collection; -import com.couchbase.client.java.ReactiveCollection; -import com.couchbase.client.java.kv.RemoveOptions; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.error.TransactionFailed; - -/** - * Tests for com.couchbase.transactions without using the spring data transactions framework - * - * @author Michael Reiche - */ -@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(Config.class) -@Disabled -public class CouchbaseTransactionNativeTests extends CollectionAwareIntegrationTests { - - // @Autowired not supported on static fields. These are initialized in beforeAll() - // Also - @Autowired doesn't work here on couchbaseClientFactory even when it is not static, not sure why - oh, it - // seems there is not a ReactiveCouchbaseClientFactory bean - static CouchbaseClientFactory couchbaseClientFactory; - static ReactiveCouchbaseOperations operations; - static GenericApplicationContext appContext; - static Transactions transactions; - @Autowired ReactivePersonRepository rxPersonRepo; - - @BeforeAll - public static void beforeAll() { - - // AnnotationConfigApplicationContext() is going to create a Transactions object. - appContext = new AnnotationConfigApplicationContext(Config.class); - operations = appContext.getBean(ReactiveCouchbaseOperations.class); - couchbaseTemplate = (CouchbaseTemplate) appContext.getBean(COUCHBASE_TEMPLATE); - transactions = (Transactions) appContext.getBean(COUCHBASE_TRANSACTIONS); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) appContext.getBean(REACTIVE_COUCHBASE_TEMPLATE); - couchbaseClientFactory = (CouchbaseClientFactory) appContext.getBean(BeanNames.COUCHBASE_CLIENT_FACTORY); - - // this will initialize couchbaseTemplate and reactiveCouchbaseTemplate if not already initialized - callSuperBeforeAll(new Object() {}); - } - - @AfterAll - public static void afterAll() { - try { - couchbaseClientFactory.close(); - } catch (IOException e) { - e.printStackTrace(); - } - appContext.close(); - callSuperAfterAll(new Object() {}); - } - - @Test - public void replacePersonCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, collectionName, person.getId().toString()); - couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return reactiveCouchbaseTemplate.findById(Person.class).inScope(null).inCollection(collectionName) - .transaction(ctx).one(person.getId().toString()).flatMap(pGet -> reactiveCouchbaseTemplate - .replaceById(Person.class).inCollection(collectionName).transaction(ctx).one(pGet.withFirstName("Walt"))) - .then(); - })); - result.block(); - Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) - .one(person.getId().toString()); - assertEquals(pFound, person.withFirstName("Walt"), "Should have found Walt"); - } - - @Test - public void insertReplacePersonsCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, collectionName, person.getId().toString()); - - Mono result = transactions.reactive((ctx) -> { // get the ctx - return reactiveCouchbaseTemplate.insertById(Person.class).inCollection(collectionName).transaction(ctx) - .one(person).flatMap(pInsert -> reactiveCouchbaseTemplate.replaceById(Person.class) - .inCollection(collectionName).transaction(ctx).one(pInsert.withFirstName("Walt"))) - .then(); - }); - - TransactionResult tr = result.block(); - Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) - .one(person.getId().toString()); - assertEquals(pFound, person.withFirstName("Walt"), "Should have found Walt"); - } - - @Test - public void deletePersonCBTransactionsRepo() { - Person person = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, collectionName, person.getId().toString()); - rxPersonRepo.withCollection(collectionName).save(person).block(); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxPersonRepo.withCollection(collectionName).withTransaction(ctx).deleteById(person.getId().toString()) - .then(rxPersonRepo.withCollection(collectionName).withTransaction(ctx).deleteById(person.getId().toString())) - .then(); - })); - assertThrows(TransactionFailed.class, () -> result.block()); - Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) - .one(person.getId().toString()); - assertEquals(pFound, person, "Should have found " + person); - } - - @Test - public void findPersonCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, collectionName, person.getId().toString()); - couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); - List docs = new LinkedList(); - Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return reactiveCouchbaseTemplate.findByQuery(Person.class).inCollection(collectionName).matching(q) - .transaction(ctx).one().map(doc -> { - docs.add(doc); - return doc; - }).then(); - })); - result.block(); - assertFalse(docs.isEmpty(), "Should have found " + person); - for (Object o : docs) { - assertEquals(o, person, "Should have found " + person); - } - } - - @Test - // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator - // Failed to retrieve PlatformTransactionManager for @Transactional test: - public void insertPersonRbCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, collectionName, person.getId().toString()); - - Mono result = transactions.reactive((ctx) -> { // get the ctx - return reactiveCouchbaseTemplate.insertById(Person.class).inCollection(collectionName).transaction(ctx) - .one(person). flatMap(it -> Mono.error(new PoofException())).then(); - }); - - try { - result.block(); - } catch (TransactionFailed e) { - e.printStackTrace(); - if (e.getCause() instanceof PoofException) { - Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) - .one(person.getId().toString()); - assertNull(pFound, "Should not have found " + pFound); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); - } - - @Test - // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator - // Failed to retrieve PlatformTransactionManager for @Transactional test: - public void replacePersonRbCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, collectionName, person.getId().toString()); - couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); - Mono result = transactions.reactive((ctx) -> { // get the ctx - return reactiveCouchbaseTemplate.findById(Person.class).inCollection(collectionName).transaction(ctx) - .one(person.getId().toString()) - .flatMap(pFound -> reactiveCouchbaseTemplate.replaceById(Person.class).inCollection(collectionName) - .transaction(ctx).one(pFound.withFirstName("Walt"))) - . flatMap(it -> Mono.error(new PoofException())).then(); - }); - - try { - result.block(); - } catch (TransactionFailed e) { - if (e.getCause() instanceof PoofException) { - Person pFound = couchbaseTemplate.findById(Person.class).inCollection(collectionName) - .one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); - } - - @Test - public void findPersonSpringTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, collectionName, person.getId().toString()); - couchbaseTemplate.insertById(Person.class).inCollection(collectionName).one(person); - List docs = new LinkedList(); - Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - Mono result = transactions.reactive((ctx) -> { // get the ctx - return reactiveCouchbaseTemplate.findByQuery(Person.class).inCollection(collectionName).matching(q) - .transaction(ctx).one().map(doc -> { - docs.add(doc); - return doc; - }).then(); - }); - result.block(); - assertFalse(docs.isEmpty(), "Should have found " + person); - for (Object o : docs) { - assertEquals(o, person, "Should have found " + person); - } - } - - void remove(Mono col, String id) { - remove(col.block(), id); - } - - void remove(Collection col, String id) { - remove(col.reactive(), id); - } - - void remove(ReactiveCollection col, String id) { - try { - col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); - } catch (DocumentNotFoundException nfe) { - System.out.println(id + " : " + "DocumentNotFound when deleting"); - } - } - - void remove(CouchbaseTemplate template, String collection, String id) { - try { - couchbaseTemplate.removeById(Person.class).inCollection(collection).one(id); - System.out.println("removed " + id); - } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { - System.out.println(id + " : " + "DocumentNotFound when deleting"); - } - } - - static class PoofException extends Exception {}; -} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save new file mode 100644 index 000000000..b6f263b54 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save @@ -0,0 +1,368 @@ +/* + * Copyright 2012-2021 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.TransactionResult; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; +import com.couchbase.transactions.error.TransactionFailed; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbaseTransactionNativeTests.Config.class) +public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { + + // @Autowired not supported on static fields. These are initialized in beforeAll() + // Also - @Autowired doesn't work here on couchbaseClientFactory even when it is not static, not sure why - oh, it + // seems there is not a ReactiveCouchbaseClientFactory bean + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired CouchbaseTransactionManager couchbaseTransactionManager; + + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + + @Autowired PersonRepository repo; + @Autowired CouchbaseTemplate cbTmpl;; + static String cName; // short name + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + // short names + cName = null;// cName; + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @Test + public void replacePersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + + try { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( + reactiveCouchbaseTransactionManager); + txOperator.execute(reactiveTransaction -> { + return cbTmpl.replaceById(Person.class).one(person); + }); + + Flux result = txOperator.execute((ctx) -> { + Person p = txOperator.template(cbTmpl).findById(Person.class).one(person.getId().toString()); + txOperator.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt")); + // throw new PoofException(); + return null; + }); + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void replacePersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + try { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( + reactiveCouchbaseTransactionManager); + TransactionResult result = txOperator + .execute((ctx) -> ctx.template(cbTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new PoofException())).then()); + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + // Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + + try { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( + reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> ctx.template(cbTmpl).insertById(Person.class) + .one(person).flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + try { + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( + reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> ctx.template(cbTmpl).insertById(Person.class) + .one(person).flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new PoofException())).then()); + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + // Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void replacePersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + repo.withCollection(cName).save(person); + try { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( + reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(repo).withCollection(cName).findById(person.getId().toString()) + .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new PoofException())).then()); + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + // Person pFound = repo.withCollection(cName).findById(person.getId().toString()); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); +try { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(couchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(repo).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) // replace + .flatMap(it -> Mono.error(new PoofException())).then()); + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + // Person pFound = repo.withCollection(cName).findById(person.getId().toString()); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void insertPersonRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); +try { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(repo).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) // replace + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + Person pFound = repo.withCollection(cName).findById(person.getId().toString()); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + @Test + public void replacePersonRbSpringTransactional() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + try { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> { + ctx.transactionResultHolder(123); + return ctx.template(cbTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .as(txOperator::transactional).then(); + }, false); + + } catch (TransactionFailed e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + } + + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + static class PoofException extends Exception {}; + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Override + public TransactionConfig getTransactionConfig() { + return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); + } + + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java new file mode 100644 index 000000000..7588e0c89 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java @@ -0,0 +1,10 @@ +package org.springframework.data.couchbase.transactions; + +import java.util.UUID; + +public class ObjectId{ + public ObjectId(){ + id = UUID.randomUUID().toString(); + } + String id; +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java new file mode 100644 index 000000000..44c68e929 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java @@ -0,0 +1,15 @@ +package org.springframework.data.couchbase.transactions; + +class SimulateFailureException extends RuntimeException { + + public SimulateFailureException(String... s){ + super(s!= null && s.length > 0 ? s[0] : null); + } + + public SimulateFailureException(){} + + public static void throwEx(String... s){ + throw new SimulateFailureException(s); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 1a033b542..05213718c 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -19,12 +19,18 @@ import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.core.service.Service; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.env.ClusterEnvironment; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -56,15 +62,22 @@ public abstract class ClusterAwareIntegrationTests { @BeforeAll static void setup(TestClusterConfig config) { testClusterConfig = config; - try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), - authenticator(), bucketName())) { - couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), - CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true)); + ClusterEnvironment env = config.seed() != null && config.seed().contains("cloud.couchbase.com") + ? ClusterEnvironment.builder() + .securityConfig(SecurityConfig.trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)) + .build() + : ClusterEnvironment.builder().build(); + String connectString = config.seed() != null && config.seed().contains("cloud.couchbase.com") ? config.seed() + : connectionString(); + try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectString, + authenticator(), bucketName(), null, env)) { + couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), CreatePrimaryQueryIndexOptions + .createPrimaryQueryIndexOptions().ignoreIfExists(true).timeout(Duration.ofSeconds(300))); // this is for the N1qlJoin test List fieldList = new ArrayList<>(); fieldList.add("parentId"); couchbaseClientFactory.getCluster().queryIndexes().createIndex(bucketName(), "parent_idx", fieldList, - CreateQueryIndexOptions.createQueryIndexOptions().ignoreIfExists(true)); + CreateQueryIndexOptions.createQueryIndexOptions().ignoreIfExists(true).timeout(Duration.ofSeconds(300))); // .with("_class", "org.springframework.data.couchbase.domain.Address")); } catch (IndexFailureException ife) { LOGGER.warn("IndexFailureException occurred - ignoring: ", ife); @@ -122,8 +135,14 @@ public static String connectionString() { } protected static Set seedNodes() { - return config().nodes().stream().map(cfg -> SeedNode.create(cfg.hostname(), - Optional.ofNullable(cfg.ports().get(Services.KV)), Optional.ofNullable(cfg.ports().get(Services.MANAGER)))) + return config().nodes().stream() + .map(cfg -> SeedNode.create(cfg.hostname(), + Optional.ofNullable(config().seed() != null && config().seed().contains("cloud.couchbase.com") + ? cfg.ports().get(Services.KV_TLS) + : cfg.ports().get(Services.KV)), + Optional.ofNullable(config().seed() != null && config().seed().contains("cloud.couchbase.com") + ? cfg.ports().get(Services.MANAGER_TLS) + : cfg.ports().get(Services.MANAGER)))) .collect(Collectors.toSet()); } diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index c535755b1..d468d0346 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -47,6 +47,9 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.junit.platform.commons.util.UnrecoverableExceptions; +import org.opentest4j.AssertionFailedError; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -111,12 +114,9 @@ public static void beforeAll() { } catch (IOException ioe) { throw new RuntimeException(ioe); } - // This will result in a Transactions object being created. - if (couchbaseTemplate == null || reactiveCouchbaseTemplate == null) { - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); - } + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); } /** @@ -146,7 +146,7 @@ protected static void createPrimaryIndex(final Cluster cluster, final String buc } public static void setupScopeCollection(Cluster cluster, String scopeName, String collectionName, - CollectionManager collectionManager) { + CollectionManager collectionManager) { // Create the scope.collection (borrowed from CollectionManagerIntegrationTest ) ScopeSpec scopeSpec = ScopeSpec.create(scopeName); CollectionSpec collSpec = CollectionSpec.create(collectionName, scopeName); @@ -224,13 +224,14 @@ protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, fin private static void createAndDeleteBucket() { final OkHttpClient httpClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build(); - String hostPort = connectionString().replace("11210", "8091"); + String hostPort = connectionString().replace("11210", "8091").replace("11207", "18091"); + String protocol = hostPort.equals("18091") ? "https" : "http"; String bucketname = UUID.randomUUID().toString(); try { Response postResponse = httpClient.newCall(new Request.Builder() .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url("http://" + hostPort + "/pools/default/buckets/") + .url(protocol+"://" + hostPort + "/pools/default/buckets/") .post(new FormBody.Builder().add("name", bucketname).add("bucketType", "membase").add("ramQuotaMB", "100") .add("replicaNumber", Integer.toString(0)).add("flushEnabled", "1").build()) .build()).execute(); @@ -240,7 +241,7 @@ private static void createAndDeleteBucket() { } Response deleteResponse = httpClient.newCall(new Request.Builder() .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url("http://" + hostPort + "/pools/default/buckets/" + bucketname).delete().build()).execute(); + .url(protocol+"://" + hostPort + "/pools/default/buckets/" + bucketname).delete().build()).execute(); System.out.println("deleteResponse: " + deleteResponse); } catch (IOException ioe) { ioe.printStackTrace(); @@ -305,7 +306,7 @@ public static boolean scopeExists(CollectionManager mgr, String scopeName) { } public static CompletableFuture createPrimaryIndex(Cluster cluster, String bucketName, String scopeName, - String collectionName) { + String collectionName) { CreatePrimaryQueryIndexOptions options = CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions(); options.timeout(Duration.ofSeconds(300)); options.ignoreIfExists(true); @@ -330,14 +331,14 @@ public static CompletableFuture createPrimaryIndex(Cluster cluster, String private static CompletableFuture exec(Cluster cluster, /*AsyncQueryIndexManager.QueryType queryType*/ boolean queryType, CharSequence statement, - Map with, CommonOptions.BuiltCommonOptions options) { + Map with, CommonOptions.BuiltCommonOptions options) { return with.isEmpty() ? exec(cluster, queryType, statement, options) : exec(cluster, queryType, statement + " WITH " + Mapper.encodeAsString(with), options); } private static CompletableFuture exec(Cluster cluster, /*AsyncQueryIndexManager.QueryType queryType,*/ boolean queryType, CharSequence statement, - CommonOptions.BuiltCommonOptions options) { + CommonOptions.BuiltCommonOptions options) { QueryOptions queryOpts = toQueryOptions(options).readonly(queryType /*requireNonNull(queryType) == READ_ONLY*/); return cluster.async().query(statement.toString(), queryOpts).exceptionally(t -> { @@ -369,7 +370,7 @@ private static RuntimeException translateException(Throwable t) { } public static void createFtsCollectionIndex(Cluster cluster, String indexName, String bucketName, String scopeName, - String collectionName) { + String collectionName) { SearchIndex searchIndex = new SearchIndex(indexName, bucketName); if (scopeName != null) { // searchIndex = searchIndex.forScopeCollection(scopeName, collectionName); @@ -406,4 +407,24 @@ public static void sleepMs(long ms) { Thread.sleep(ms); } catch (InterruptedException ie) {} } + + public static Throwable assertThrowsOneOf(Executable executable, Class... expectedTypes) { + + try { + executable.execute(); + } + catch (Throwable actualException) { + for(Class expectedType:expectedTypes){ + if(actualException.getClass().isAssignableFrom( expectedType)){ + return actualException; + } + } + UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + String message = "Unexpected exception type thrown "+actualException.getClass(); + throw new AssertionFailedError(message, actualException); + } + + String message ="Expected "+expectedTypes+" to be thrown, but nothing was thrown."; + throw new AssertionFailedError(message); + } } diff --git a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java index 36cbbe8df..0ec98b390 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.util; -import static java.nio.charset.StandardCharsets.*; +import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.net.URL; diff --git a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java index bdaac406a..3dc867ac3 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java @@ -79,6 +79,7 @@ public Optional clusterCert() { * Finds the first node with a given service enabled in the config. *

    * This method can be used to find bootstrap nodes and similar. + *

    * * @param service the service to find. * @return a node config if found, empty otherwise. From 0165fdb03821d7b84590dd3dea78877524692df4 Mon Sep 17 00:00:00 2001 From: Michael Reiche Date: Tue, 12 Apr 2022 09:48:10 -0700 Subject: [PATCH 3/9] Transaction Support. --- .mvn/wrapper/maven-wrapper.jar | Bin 48337 -> 0 bytes .../SimpleCouchbaseClientFactory.java | 39 ++++++------------ ...basePersonTransactionIntegrationTests.java | 12 ++---- .../util/ClusterAwareIntegrationTests.java | 17 ++------ src/test/resources/integration.properties | 4 +- 5 files changed, 22 insertions(+), 50 deletions(-) delete mode 100755 .mvn/wrapper/maven-wrapper.jar diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100755 index 01e67997377a393fd672c7dcde9dccbedf0cb1e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48337 zcmbTe1CV9Qwl>;j+wQV$+qSXFw%KK)%eHN!%U!l@+x~l>b1vR}@9y}|TM-#CBjy|< zb7YRpp)Z$$Gzci_H%LgxZ{NNV{%Qa9gZlF*E2<($D=8;N5Asbx8se{Sz5)O13x)rc z5cR(k$_mO!iis+#(8-D=#R@|AF(8UQ`L7dVNSKQ%v^P|1A%aF~Lye$@HcO@sMYOb3 zl`5!ThJ1xSJwsg7hVYFtE5vS^5UE0$iDGCS{}RO;R#3y#{w-1hVSg*f1)7^vfkxrm!!N|oTR0Hj?N~IbVk+yC#NK} z5myv()UMzV^!zkX@O=Yf!(Z_bF7}W>k*U4@--&RH0tHiHY0IpeezqrF#@8{E$9d=- z7^kT=1Bl;(Q0k{*_vzz1Et{+*lbz%mkIOw(UA8)EE-Pkp{JtJhe@VXQ8sPNTn$Vkj zicVp)sV%0omhsj;NCmI0l8zzAipDV#tp(Jr7p_BlL$}Pys_SoljztS%G-Wg+t z&Q#=<03Hoga0R1&L!B);r{Cf~b$G5p#@?R-NNXMS8@cTWE^7V!?ixz(Ag>lld;>COenWc$RZ61W+pOW0wh>sN{~j; zCBj!2nn|4~COwSgXHFH?BDr8pK323zvmDK-84ESq25b;Tg%9(%NneBcs3;r znZpzntG%E^XsSh|md^r-k0Oen5qE@awGLfpg;8P@a-s<{Fwf?w3WapWe|b-CQkqlo z46GmTdPtkGYdI$e(d9Zl=?TU&uv94VR`g|=7xB2Ur%=6id&R2 z4e@fP7`y58O2sl;YBCQFu7>0(lVt-r$9|06Q5V>4=>ycnT}Fyz#9p;3?86`ZD23@7 z7n&`!LXzjxyg*P4Tz`>WVvpU9-<5MDSDcb1 zZaUyN@7mKLEPGS$^odZcW=GLe?3E$JsMR0kcL4#Z=b4P94Q#7O%_60{h>0D(6P*VH z3}>$stt2s!)w4C4 z{zsj!EyQm$2ARSHiRm49r7u)59ZyE}ZznFE7AdF&O&!-&(y=?-7$LWcn4L_Yj%w`qzwz`cLqPRem1zN; z)r)07;JFTnPODe09Z)SF5@^uRuGP~Mjil??oWmJTaCb;yx4?T?d**;AW!pOC^@GnT zaY`WF609J>fG+h?5&#}OD1<%&;_lzM2vw70FNwn2U`-jMH7bJxdQM#6+dPNiiRFGT z7zc{F6bo_V%NILyM?rBnNsH2>Bx~zj)pJ}*FJxW^DC2NLlOI~18Mk`7sl=t`)To6Ui zu4GK6KJx^6Ms4PP?jTn~jW6TOFLl3e2-q&ftT=31P1~a1%7=1XB z+H~<1dh6%L)PbBmtsAr38>m~)?k3}<->1Bs+;227M@?!S+%X&M49o_e)X8|vZiLVa z;zWb1gYokP;Sbao^qD+2ZD_kUn=m=d{Q9_kpGxcbdQ0d5<_OZJ!bZJcmgBRf z!Cdh`qQ_1NLhCulgn{V`C%|wLE8E6vq1Ogm`wb;7Dj+xpwik~?kEzDT$LS?#%!@_{ zhOoXOC95lVcQU^pK5x$Da$TscVXo19Pps zA!(Mk>N|tskqBn=a#aDC4K%jV#+qI$$dPOK6;fPO)0$0j$`OV+mWhE+TqJoF5dgA=TH-}5DH_)H_ zh?b(tUu@65G-O)1ah%|CsU8>cLEy0!Y~#ut#Q|UT92MZok0b4V1INUL-)Dvvq`RZ4 zTU)YVX^r%_lXpn_cwv`H=y49?!m{krF3Rh7O z^z7l4D<+^7E?ji(L5CptsPGttD+Z7{N6c-`0V^lfFjsdO{aJMFfLG9+wClt<=Rj&G zf6NgsPSKMrK6@Kvgarmx{&S48uc+ZLIvk0fbH}q-HQ4FSR33$+%FvNEusl6xin!?e z@rrWUP5U?MbBDeYSO~L;S$hjxISwLr&0BOSd?fOyeCWm6hD~)|_9#jo+PVbAY3wzf zcZS*2pX+8EHD~LdAl>sA*P>`g>>+&B{l94LNLp#KmC)t6`EPhL95s&MMph46Sk^9x%B$RK!2MI--j8nvN31MNLAJBsG`+WMvo1}xpaoq z%+W95_I`J1Pr&Xj`=)eN9!Yt?LWKs3-`7nf)`G6#6#f+=JK!v943*F&veRQxKy-dm(VcnmA?K_l~ zfDWPYl6hhN?17d~^6Zuo@>Hswhq@HrQ)sb7KK^TRhaM2f&td)$6zOn7we@ zd)x4-`?!qzTGDNS-E(^mjM%d46n>vPeMa;%7IJDT(nC)T+WM5F-M$|p(78W!^ck6)A_!6|1o!D97tw8k|5@0(!8W&q9*ovYl)afk z2mxnniCOSh7yHcSoEu8k`i15#oOi^O>uO_oMpT=KQx4Ou{&C4vqZG}YD0q!{RX=`#5wmcHT=hqW3;Yvg5Y^^ ziVunz9V)>2&b^rI{ssTPx26OxTuCw|+{tt_M0TqD?Bg7cWN4 z%UH{38(EW1L^!b~rtWl)#i}=8IUa_oU8**_UEIw+SYMekH;Epx*SA7Hf!EN&t!)zuUca@_Q^zW(u_iK_ zrSw{nva4E6-Npy9?lHAa;b(O z`I74A{jNEXj(#r|eS^Vfj-I!aHv{fEkzv4=F%z0m;3^PXa27k0Hq#RN@J7TwQT4u7 ztisbp3w6#k!RC~!5g-RyjpTth$lf!5HIY_5pfZ8k#q!=q*n>~@93dD|V>=GvH^`zn zVNwT@LfA8^4rpWz%FqcmzX2qEAhQ|_#u}md1$6G9qD%FXLw;fWWvqudd_m+PzI~g3 z`#WPz`M1XUKfT3&T4~XkUie-C#E`GN#P~S(Zx9%CY?EC?KP5KNK`aLlI1;pJvq@d z&0wI|dx##t6Gut6%Y9c-L|+kMov(7Oay++QemvI`JOle{8iE|2kZb=4x%a32?>-B~ z-%W$0t&=mr+WJ3o8d(|^209BapD`@6IMLbcBlWZlrr*Yrn^uRC1(}BGNr!ct z>xzEMV(&;ExHj5cce`pk%6!Xu=)QWtx2gfrAkJY@AZlHWiEe%^_}mdzvs(6>k7$e; ze4i;rv$_Z$K>1Yo9f4&Jbx80?@X!+S{&QwA3j#sAA4U4#v zwZqJ8%l~t7V+~BT%j4Bwga#Aq0&#rBl6p$QFqS{DalLd~MNR8Fru+cdoQ78Dl^K}@l#pmH1-e3?_0tZKdj@d2qu z_{-B11*iuywLJgGUUxI|aen-((KcAZZdu8685Zi1b(#@_pmyAwTr?}#O7zNB7U6P3 zD=_g*ZqJkg_9_X3lStTA-ENl1r>Q?p$X{6wU6~e7OKNIX_l9T# z>XS?PlNEM>P&ycY3sbivwJYAqbQH^)z@PobVRER*Ud*bUi-hjADId`5WqlZ&o+^x= z-Lf_80rC9>tqFBF%x#`o>69>D5f5Kp->>YPi5ArvgDwV#I6!UoP_F0YtfKoF2YduA zCU!1`EB5;r68;WyeL-;(1K2!9sP)at9C?$hhy(dfKKBf}>skPqvcRl>UTAB05SRW! z;`}sPVFFZ4I%YrPEtEsF(|F8gnfGkXI-2DLsj4_>%$_ZX8zVPrO=_$7412)Mr9BH{ zwKD;e13jP2XK&EpbhD-|`T~aI`N(*}*@yeDUr^;-J_`fl*NTSNbupyHLxMxjwmbuw zt3@H|(hvcRldE+OHGL1Y;jtBN76Ioxm@UF1K}DPbgzf_a{`ohXp_u4=ps@x-6-ZT>F z)dU`Jpu~Xn&Qkq2kg%VsM?mKC)ArP5c%r8m4aLqimgTK$atIxt^b8lDVPEGDOJu!) z%rvASo5|v`u_}vleP#wyu1$L5Ta%9YOyS5;w2I!UG&nG0t2YL|DWxr#T7P#Ww8MXDg;-gr`x1?|V`wy&0vm z=hqozzA!zqjOm~*DSI9jk8(9nc4^PL6VOS$?&^!o^Td8z0|eU$9x8s{8H!9zK|)NO zqvK*dKfzG^Dy^vkZU|p9c+uVV3>esY)8SU1v4o{dZ+dPP$OT@XCB&@GJ<5U&$Pw#iQ9qzuc`I_%uT@%-v zLf|?9w=mc;b0G%%{o==Z7AIn{nHk`>(!e(QG%(DN75xfc#H&S)DzSFB6`J(cH!@mX3mv_!BJv?ByIN%r-i{Y zBJU)}Vhu)6oGoQjT2tw&tt4n=9=S*nQV`D_MSw7V8u1-$TE>F-R6Vo0giKnEc4NYZ zAk2$+Tba~}N0wG{$_7eaoCeb*Ubc0 zq~id50^$U>WZjmcnIgsDione)f+T)0ID$xtgM zpGZXmVez0DN!)ioW1E45{!`G9^Y1P1oXhP^rc@c?o+c$^Kj_bn(Uo1H2$|g7=92v- z%Syv9Vo3VcibvH)b78USOTwIh{3%;3skO_htlfS?Cluwe`p&TMwo_WK6Z3Tz#nOoy z_E17(!pJ>`C2KECOo38F1uP0hqBr>%E=LCCCG{j6$b?;r?Fd$4@V-qjEzgWvzbQN%_nlBg?Ly`x-BzO2Nnd1 zuO|li(oo^Rubh?@$q8RVYn*aLnlWO_dhx8y(qzXN6~j>}-^Cuq4>=d|I>vhcjzhSO zU`lu_UZ?JaNs1nH$I1Ww+NJI32^qUikAUfz&k!gM&E_L=e_9}!<(?BfH~aCmI&hfzHi1~ zraRkci>zMPLkad=A&NEnVtQQ#YO8Xh&K*;6pMm$ap_38m;XQej5zEqUr`HdP&cf0i z5DX_c86@15jlm*F}u-+a*^v%u_hpzwN2eT66Zj_1w)UdPz*jI|fJb#kSD_8Q-7q9gf}zNu2h=q{)O*XH8FU)l|m;I;rV^QpXRvMJ|7% zWKTBX*cn`VY6k>mS#cq!uNw7H=GW3?wM$8@odjh$ynPiV7=Ownp}-|fhULZ)5{Z!Q z20oT!6BZTK;-zh=i~RQ$Jw>BTA=T(J)WdnTObDM#61lUm>IFRy@QJ3RBZr)A9CN!T z4k7%)I4yZ-0_n5d083t!=YcpSJ}M5E8`{uIs3L0lIaQws1l2}+w2(}hW&evDlMnC!WV?9U^YXF}!N*iyBGyCyJ<(2(Ca<>!$rID`( zR?V~-53&$6%DhW=)Hbd-oetTXJ-&XykowOx61}1f`V?LF=n8Nb-RLFGqheS7zNM_0 z1ozNap9J4GIM1CHj-%chrCdqPlP307wfrr^=XciOqn?YPL1|ozZ#LNj8QoCtAzY^q z7&b^^K&?fNSWD@*`&I+`l9 zP2SlD0IO?MK60nbucIQWgz85l#+*<{*SKk1K~|x{ux+hn=SvE_XE`oFlr7$oHt-&7 zP{+x)*y}Hnt?WKs_Ymf(J^aoe2(wsMMRPu>Pg8H#x|zQ_=(G5&ieVhvjEXHg1zY?U zW-hcH!DJPr+6Xnt)MslitmnHN(Kgs4)Y`PFcV0Qvemj;GG`kf<>?p})@kd9DA7dqs zNtGRKVr0%x#Yo*lXN+vT;TC{MR}}4JvUHJHDLd-g88unUj1(#7CM<%r!Z1Ve>DD)FneZ| z8Q0yI@i4asJaJ^ge%JPl>zC3+UZ;UDUr7JvUYNMf=M2t{It56OW1nw#K8%sXdX$Yg zpw3T=n}Om?j3-7lu)^XfBQkoaZ(qF0D=Aw&D%-bsox~`8Y|!whzpd5JZ{dmM^A5)M zOwWEM>bj}~885z9bo{kWFA0H(hv(vL$G2;pF$@_M%DSH#g%V*R(>;7Z7eKX&AQv1~ z+lKq=488TbTwA!VtgSHwduwAkGycunrg}>6oiX~;Kv@cZlz=E}POn%BWt{EEd;*GV zmc%PiT~k<(TA`J$#6HVg2HzF6Iw5w9{C63y`Y7?OB$WsC$~6WMm3`UHaWRZLN3nKiV# zE;iiu_)wTr7ZiELH$M^!i5eC9aRU#-RYZhCl1z_aNs@f`tD4A^$xd7I_ijCgI!$+| zsulIT$KB&PZ}T-G;Ibh@UPafvOc-=p7{H-~P)s{3M+;PmXe7}}&Mn+9WT#(Jmt5DW%73OBA$tC#Ug!j1BR~=Xbnaz4hGq zUOjC*z3mKNbrJm1Q!Ft^5{Nd54Q-O7<;n})TTQeLDY3C}RBGwhy*&wgnl8dB4lwkG zBX6Xn#hn|!v7fp@@tj9mUPrdD!9B;tJh8-$aE^t26n_<4^=u~s_MfbD?lHnSd^FGGL6the7a|AbltRGhfET*X;P7=AL?WPjBtt;3IXgUHLFMRBz(aWW_ zZ?%%SEPFu&+O?{JgTNB6^5nR@)rL6DFqK$KS$bvE#&hrPs>sYsW=?XzOyD6ixglJ8rdt{P8 zPAa*+qKt(%ju&jDkbB6x7aE(={xIb*&l=GF(yEnWPj)><_8U5m#gQIIa@l49W_=Qn^RCsYqlEy6Om%!&e~6mCAfDgeXe3aYpHQAA!N|kmIW~Rk}+p6B2U5@|1@7iVbm5&e7E3;c9q@XQlb^JS(gmJl%j9!N|eNQ$*OZf`3!;raRLJ z;X-h>nvB=S?mG!-VH{65kwX-UwNRMQB9S3ZRf`hL z#WR)+rn4C(AG(T*FU}`&UJOU4#wT&oDyZfHP^s9#>V@ens??pxuu-6RCk=Er`DF)X z>yH=P9RtrtY;2|Zg3Tnx3Vb!(lRLedVRmK##_#;Kjnlwq)eTbsY8|D{@Pjn_=kGYO zJq0T<_b;aB37{U`5g6OSG=>|pkj&PohM%*O#>kCPGK2{0*=m(-gKBEOh`fFa6*~Z! zVxw@7BS%e?cV^8{a`Ys4;w=tH4&0izFxgqjE#}UfsE^?w)cYEQjlU|uuv6{>nFTp| zNLjRRT1{g{?U2b6C^w{!s+LQ(n}FfQPDfYPsNV?KH_1HgscqG7z&n3Bh|xNYW4i5i zT4Uv-&mXciu3ej=+4X9h2uBW9o(SF*N~%4%=g|48R-~N32QNq!*{M4~Y!cS4+N=Zr z?32_`YpAeg5&r_hdhJkI4|i(-&BxCKru`zm9`v+CN8p3r9P_RHfr{U$H~RddyZKw{ zR?g5i>ad^Ge&h?LHlP7l%4uvOv_n&WGc$vhn}2d!xIWrPV|%x#2Q-cCbQqQ|-yoTe z_C(P))5e*WtmpB`Fa~#b*yl#vL4D_h;CidEbI9tsE%+{-4ZLKh#9^{mvY24#u}S6oiUr8b0xLYaga!(Fe7Dxi}v6 z%5xNDa~i%tN`Cy_6jbk@aMaY(xO2#vWZh9U?mrNrLs5-*n>04(-Dlp%6AXsy;f|a+ z^g~X2LhLA>xy(8aNL9U2wr=ec%;J2hEyOkL*D%t4cNg7WZF@m?kF5YGvCy`L5jus# zGP8@iGTY|ov#t&F$%gkWDoMR7v*UezIWMeg$C2~WE9*5%}$3!eFiFJ?hypfIA(PQT@=B|^Ipcu z{9cM3?rPF|gM~{G)j*af1hm+l92W7HRpQ*hSMDbh(auwr}VBG7`ldp>`FZ^amvau zTa~Y7%tH@>|BB6kSRGiWZFK?MIzxEHKGz#P!>rB-90Q_UsZ=uW6aTzxY{MPP@1rw- z&RP^Ld%HTo($y?6*aNMz8h&E?_PiO{jq%u4kr#*uN&Q+Yg1Rn831U4A6u#XOzaSL4 zrcM+0v@%On8N*Mj!)&IzXW6A80bUK&3w|z06cP!UD^?_rb_(L-u$m+#%YilEjkrlxthGCLQ@Q?J!p?ggv~0 z!qipxy&`w48T0(Elsz<^hp_^#1O1cNJ1UG=61Nc=)rlRo_P6v&&h??Qvv$ifC3oJh zo)ZZhU5enAqU%YB>+FU!1vW)i$m-Z%w!c&92M1?))n4z1a#4-FufZ$DatpJ^q)_Zif z;Br{HmZ|8LYRTi`#?TUfd;#>c4@2qM5_(H+Clt@kkQT+kx78KACyvY)?^zhyuN_Z& z-*9_o_f3IC2lX^(aLeqv#>qnelb6_jk+lgQh;TN>+6AU9*6O2h_*=74m;xSPD1^C9 zE0#!+B;utJ@8P6_DKTQ9kNOf`C*Jj0QAzsngKMQVDUsp=k~hd@wt}f{@$O*xI!a?p z6Gti>uE}IKAaQwKHRb0DjmhaF#+{9*=*^0)M-~6lPS-kCI#RFGJ-GyaQ+rhbmhQef zwco))WNA1LFr|J3Qsp4ra=_j?Y%b{JWMX6Zr`$;*V`l`g7P0sP?Y1yOY;e0Sb!AOW0Em=U8&i8EKxTd$dX6=^Iq5ZC%zMT5Jjj%0_ zbf|}I=pWjBKAx7wY<4-4o&E6vVStcNlT?I18f5TYP9!s|5yQ_C!MNnRyDt7~u~^VS@kKd}Zwc~? z=_;2}`Zl^xl3f?ce8$}g^V)`b8Pz88=9FwYuK_x%R?sbAF-dw`*@wokEC3mp0Id>P z>OpMGxtx!um8@gW2#5|)RHpRez+)}_p;`+|*m&3&qy{b@X>uphcgAVgWy`?Nc|NlH z75_k2%3h7Fy~EkO{vBMuzV7lj4B}*1Cj(Ew7oltspA6`d69P`q#Y+rHr5-m5&be&( zS1GcP5u#aM9V{fUQTfHSYU`kW&Wsxeg;S*{H_CdZ$?N>S$JPv!_6T(NqYPaS{yp0H7F~7vy#>UHJr^lV?=^vt4?8$v8vkI-1eJ4{iZ!7D5A zg_!ZxZV+9Wx5EIZ1%rbg8`-m|=>knmTE1cpaBVew_iZpC1>d>qd3`b6<(-)mtJBmd zjuq-qIxyKvIs!w4$qpl{0cp^-oq<=-IDEYV7{pvfBM7tU+ zfX3fc+VGtqjPIIx`^I0i>*L-NfY=gFS+|sC75Cg;2<)!Y`&p&-AxfOHVADHSv1?7t zlOKyXxi|7HdwG5s4T0))dWudvz8SZpxd<{z&rT<34l}XaaP86x)Q=2u5}1@Sgc41D z2gF)|aD7}UVy)bnm788oYp}Es!?|j73=tU<_+A4s5&it~_K4 z;^$i0Vnz8y&I!abOkzN|Vz;kUTya#Wi07>}Xf^7joZMiHH3Mdy@e_7t?l8^A!r#jTBau^wn#{|!tTg=w01EQUKJOca!I zV*>St2399#)bMF++1qS8T2iO3^oA`i^Px*i)T_=j=H^Kp4$Zao(>Y)kpZ=l#dSgcUqY=7QbGz9mP9lHnII8vl?yY9rU+i%X)-j0&-- zrtaJsbkQ$;DXyIqDqqq)LIJQ!`MIsI;goVbW}73clAjN;1Rtp7%{67uAfFNe_hyk= zn=8Q1x*zHR?txU)x9$nQu~nq7{Gbh7?tbgJ>i8%QX3Y8%T{^58W^{}(!9oPOM+zF3 zW`%<~q@W}9hoes56uZnNdLkgtcRqPQ%W8>o7mS(j5Sq_nN=b0A`Hr%13P{uvH?25L zMfC&Z0!{JBGiKoVwcIhbbx{I35o}twdI_ckbs%1%AQ(Tdb~Xw+sXAYcOoH_9WS(yM z2dIzNLy4D%le8Fxa31fd;5SuW?ERAsagZVEo^i};yjBhbxy9&*XChFtOPV8G77{8! zlYemh2vp7aBDMGT;YO#=YltE~(Qv~e7c=6$VKOxHwvrehtq>n|w}vY*YvXB%a58}n zqEBR4zueP@A~uQ2x~W-{o3|-xS@o>Ad@W99)ya--dRx;TZLL?5E(xstg(6SwDIpL5 zMZ)+)+&(hYL(--dxIKB*#v4mDq=0ve zNU~~jk426bXlS8%lcqsvuqbpgn zbFgxap;17;@xVh+Y~9@+-lX@LQv^Mw=yCM&2!%VCfZsiwN>DI=O?vHupbv9!4d*>K zcj@a5vqjcjpwkm@!2dxzzJGQ7#ujW(IndUuYC)i3N2<*doRGX8a$bSbyRO#0rA zUpFyEGx4S9$TKuP9BybRtjcAn$bGH-9>e(V{pKYPM3waYrihBCQf+UmIC#E=9v?or z_7*yzZfT|)8R6>s(lv6uzosT%WoR`bQIv(?llcH2Bd@26?zU%r1K25qscRrE1 z9TIIP_?`78@uJ{%I|_K;*syVinV;pCW!+zY-!^#n{3It^6EKw{~WIA0pf_hVzEZy zFzE=d-NC#mge{4Fn}we02-%Zh$JHKpXX3qF<#8__*I}+)Npxm?26dgldWyCmtwr9c zOXI|P0zCzn8M_Auv*h9;2lG}x*E|u2!*-s}moqS%Z`?O$<0amJG9n`dOV4**mypG- zE}In1pOQ|;@@Jm;I#m}jkQegIXag4K%J;C7<@R2X8IdsCNqrbsaUZZRT|#6=N!~H} zlc2hPngy9r+Gm_%tr9V&HetvI#QwUBKV&6NC~PK>HNQ3@fHz;J&rR7XB>sWkXKp%A ziLlogA`I*$Z7KzLaX^H_j)6R|9Q>IHc? z{s0MsOW>%xW|JW=RUxY@@0!toq`QXa=`j;)o2iDBiDZ7c4Bc>BiDTw+zk}Jm&vvH8qX$R`M6Owo>m%n`eizBf!&9X6 z)f{GpMak@NWF+HNg*t#H5yift5@QhoYgT7)jxvl&O=U54Z>FxT5prvlDER}AwrK4Q z*&JP9^k332OxC$(E6^H`#zw|K#cpwy0i*+!z{T23;dqUKbjP!-r*@_!sp+Uec@^f0 zIJMjqhp?A#YoX5EB%iWu;mxJ1&W6Nb4QQ@GElqNjFNRc*=@aGc$PHdoUptckkoOZC zk@c9i+WVnDI=GZ1?lKjobDl%nY2vW~d)eS6Lch&J zDi~}*fzj9#<%xg<5z-4(c}V4*pj~1z2z60gZc}sAmys^yvobWz)DKDGWuVpp^4-(!2Nn7 z3pO})bO)({KboXlQA>3PIlg@Ie$a=G;MzVeft@OMcKEjIr=?;=G0AH?dE_DcNo%n$_bFjqQ8GjeIyJP^NkX~7e&@+PqnU-c3@ABap z=}IZvC0N{@fMDOpatOp*LZ7J6Hz@XnJzD!Yh|S8p2O($2>A4hbpW{8?#WM`uJG>?} zwkDF3dimqejl$3uYoE7&pr5^f4QP-5TvJ;5^M?ZeJM8ywZ#Dm`kR)tpYieQU;t2S! z05~aeOBqKMb+`vZ2zfR*2(&z`Y1VROAcR(^Q7ZyYlFCLHSrTOQm;pnhf3Y@WW#gC1 z7b$_W*ia0@2grK??$pMHK>a$;J)xIx&fALD4)w=xlT=EzrwD!)1g$2q zy8GQ+r8N@?^_tuCKVi*q_G*!#NxxY#hpaV~hF} zF1xXy#XS|q#)`SMAA|46+UnJZ__lETDwy}uecTSfz69@YO)u&QORO~F^>^^j-6q?V z-WK*o?XSw~ukjoIT9p6$6*OStr`=+;HrF#)p>*>e|gy0D9G z#TN(VSC11^F}H#?^|^ona|%;xCC!~H3~+a>vjyRC5MPGxFqkj6 zttv9I_fv+5$vWl2r8+pXP&^yudvLxP44;9XzUr&a$&`?VNhU^$J z`3m68BAuA?ia*IF%Hs)@>xre4W0YoB^(X8RwlZ?pKR)rvGX?u&K`kb8XBs^pe}2v* z_NS*z7;4%Be$ts_emapc#zKjVMEqn8;aCX=dISG3zvJP>l4zHdpUwARLixQSFzLZ0 z$$Q+9fAnVjA?7PqANPiH*XH~VhrVfW11#NkAKjfjQN-UNz?ZT}SG#*sk*)VUXZ1$P zdxiM@I2RI7Tr043ZgWd3G^k56$Non@LKE|zLwBgXW#e~{7C{iB3&UjhKZPEj#)cH9 z%HUDubc0u@}dBz>4zU;sTluxBtCl!O4>g9ywc zhEiM-!|!C&LMjMNs6dr6Q!h{nvTrNN0hJ+w*h+EfxW=ro zxAB%*!~&)uaqXyuh~O`J(6e!YsD0o0l_ung1rCAZt~%4R{#izD2jT~${>f}m{O!i4 z`#UGbiSh{L=FR`Q`e~9wrKHSj?I>eXHduB`;%TcCTYNG<)l@A%*Ld?PK=fJi}J? z9T-|Ib8*rLE)v_3|1+Hqa!0ch>f% zfNFz@o6r5S`QQJCwRa4zgx$7AyQ7ZTv2EM7ZQHh!72CFL+qT`Y)k!)|Zr;7mcfV8T z)PB$1r*5rUzgE@y^E_kDG3Ol5n6q}eU2hJcXY7PI1}N=>nwC6k%nqxBIAx4Eix*`W zch0}3aPFe5*lg1P(=7J^0ZXvpOi9v2l*b?j>dI%iamGp$SmFaxpZod*TgYiyhF0= za44lXRu%9MA~QWN;YX@8LM32BqKs&W4&a3ve9C~ndQq>S{zjRNj9&&8k-?>si8)^m zW%~)EU)*$2YJzTXjRV=-dPAu;;n2EDYb=6XFyz`D0f2#29(mUX}*5~KU3k>$LwN#OvBx@ zl6lC>UnN#0?mK9*+*DMiboas!mmGnoG%gSYeThXI<=rE(!Pf-}oW}?yDY0804dH3o zo;RMFJzxP|srP-6ZmZ_peiVycfvH<`WJa9R`Z#suW3KrI*>cECF(_CB({ToWXSS18#3%vihZZJ{BwJPa?m^(6xyd1(oidUkrOU zlqyRQUbb@W_C)5Q)%5bT3K0l)w(2cJ-%?R>wK35XNl&}JR&Pn*laf1M#|s4yVXQS# zJvkT$HR;^3k{6C{E+{`)J+~=mPA%lv1T|r#kN8kZP}os;n39exCXz^cc{AN(Ksc%} zA561&OeQU8gIQ5U&Y;Ca1TatzG`K6*`9LV<|GL-^=qg+nOx~6 zBEMIM7Q^rkuhMtw(CZtpU(%JlBeV?KC+kjVDL34GG1sac&6(XN>nd+@Loqjo%i6I~ zjNKFm^n}K=`z8EugP20fd_%~$Nfu(J(sLL1gvXhxZt|uvibd6rLXvM%!s2{g0oNA8 z#Q~RfoW8T?HE{ge3W>L9bx1s2_L83Odx)u1XUo<`?a~V-_ZlCeB=N-RWHfs1(Yj!_ zP@oxCRysp9H8Yy@6qIc69TQx(1P`{iCh)8_kH)_vw1=*5JXLD(njxE?2vkOJ z>qQz!*r`>X!I69i#1ogdVVB=TB40sVHX;gak=fu27xf*}n^d>@*f~qbtVMEW!_|+2 zXS`-E%v`_>(m2sQnc6+OA3R z-6K{6$KZsM+lF&sn~w4u_md6J#+FzqmtncY;_ z-Q^D=%LVM{A0@VCf zV9;?kF?vV}*=N@FgqC>n-QhKJD+IT7J!6llTEH2nmUxKiBa*DO4&PD5=HwuD$aa(1 z+uGf}UT40OZAH@$jjWoI7FjOQAGX6roHvf_wiFKBfe4w|YV{V;le}#aT3_Bh^$`Pp zJZGM_()iFy#@8I^t{ryOKQLt%kF7xq&ZeD$$ghlTh@bLMv~||?Z$#B2_A4M&8)PT{ zyq$BzJpRrj+=?F}zH+8XcPvhRP+a(nnX2^#LbZqgWQ7uydmIM&FlXNx4o6m;Q5}rB z^ryM&o|~a-Zb20>UCfSFwdK4zfk$*~<|90v0=^!I?JnHBE{N}74iN;w6XS=#79G+P zB|iewe$kk;9^4LinO>)~KIT%%4Io6iFFXV9gJcIvu-(!um{WfKAwZDmTrv=wb#|71 zWqRjN8{3cRq4Ha2r5{tw^S>0DhaC3m!i}tk9q08o>6PtUx1GsUd{Z17FH45rIoS+oym1>3S0B`>;uo``+ADrd_Um+8s$8V6tKsA8KhAm z{pTv@zj~@+{~g&ewEBD3um9@q!23V_8Nb0_R#1jcg0|MyU)?7ua~tEY63XSvqwD`D zJ+qY0Wia^BxCtXpB)X6htj~*7)%un+HYgSsSJPAFED7*WdtlFhuJj5d3!h8gt6$(s ztrx=0hFH8z(Fi9}=kvPI?07j&KTkssT=Vk!d{-M50r!TsMD8fPqhN&%(m5LGpO>}L zse;sGl_>63FJ)(8&8(7Wo2&|~G!Lr^cc!uuUBxGZE)ac7Jtww7euxPo)MvxLXQXlk zeE>E*nMqAPwW0&r3*!o`S7wK&078Q#1bh!hNbAw0MFnK-2gU25&8R@@j5}^5-kHeR z!%krca(JG%&qL2mjFv380Gvb*eTLllTaIpVr3$gLH2e3^xo z=qXjG0VmES%OXAIsOQG|>{aj3fv+ZWdoo+a9tu8)4AyntBP>+}5VEmv@WtpTo<-aH zF4C(M#dL)MyZmU3sl*=TpAqU#r>c8f?-zWMq`wjEcp^jG2H`8m$p-%TW?n#E5#Th+ z7Zy#D>PPOA4|G@-I$!#Yees_9Ku{i_Y%GQyM)_*u^nl+bXMH!f_ z8>BM|OTex;vYWu`AhgfXFn)0~--Z7E0WR-v|n$XB-NOvjM156WR(eu z(qKJvJ%0n+%+%YQP=2Iz-hkgI_R>7+=)#FWjM#M~Y1xM8m_t8%=FxV~Np$BJ{^rg9 z5(BOvYfIY{$h1+IJyz-h`@jhU1g^Mo4K`vQvR<3wrynWD>p{*S!kre-(MT&`7-WK! zS}2ceK+{KF1yY*x7FH&E-1^8b$zrD~Ny9|9(!1Y)a#)*zf^Uo@gy~#%+*u`U!R`^v zCJ#N!^*u_gFq7;-XIYKXvac$_=booOzPgrMBkonnn%@#{srUC<((e*&7@YR?`CP;o zD2*OE0c%EsrI72QiN`3FpJ#^Bgf2~qOa#PHVmbzonW=dcrs92>6#{pEnw19AWk%;H zJ4uqiD-dx*w2pHf8&Jy{NXvGF^Gg!ungr2StHpMQK5^+ zEmDjjBonrrT?d9X;BHSJeU@lX19|?On)(Lz2y-_;_!|}QQMsq4Ww9SmzGkzVPQTr* z)YN>_8i^rTM>Bz@%!!v)UsF&Nb{Abz>`1msFHcf{)Ufc_a-mYUPo@ei#*%I_jWm#7 zX01=Jo<@6tl`c;P_uri^gJxDVHOpCano2Xc5jJE8(;r@y6THDE>x*#-hSKuMQ_@nc z68-JLZyag_BTRE(B)Pw{B;L0+Zx!5jf%z-Zqug*og@^ zs{y3{Za(0ywO6zYvES>SW*cd4gwCN^o9KQYF)Lm^hzr$w&spGNah6g>EQBufQCN!y zI5WH$K#67$+ic{yKAsX@el=SbBcjRId*cs~xk~3BBpQsf%IsoPG)LGs zdK0_rwz7?L0XGC^2$dktLQ9qjwMsc1rpGx2Yt?zmYvUGnURx(1k!kmfPUC@2Pv;r9 z`-Heo+_sn+!QUJTAt;uS_z5SL-GWQc#pe0uA+^MCWH=d~s*h$XtlN)uCI4$KDm4L$ zIBA|m0o6@?%4HtAHRcDwmzd^(5|KwZ89#UKor)8zNI^EsrIk z1QLDBnNU1!PpE3iQg9^HI){x7QXQV{&D>2U%b_II>*2*HF2%>KZ>bxM)Jx4}|CCEa`186nD_B9h`mv6l45vRp*L+z_nx5i#9KvHi>rqxJIjKOeG(5lCeo zLC|-b(JL3YP1Ds=t;U!Y&Gln*Uwc0TnDSZCnh3m$N=xWMcs~&Rb?w}l51ubtz=QUZsWQhWOX;*AYb)o(^<$zU_v=cFwN~ZVrlSLx| zpr)Q7!_v*%U}!@PAnZLqOZ&EbviFbej-GwbeyaTq)HSBB+tLH=-nv1{MJ-rGW%uQ1 znDgP2bU@}!Gd=-;3`KlJYqB@U#Iq8Ynl%eE!9g;d*2|PbC{A}>mgAc8LK<69qcm)piu?`y~3K8zlZ1>~K_4T{%4zJG6H?6%{q3B-}iP_SGXELeSv*bvBq~^&C=3TsP z9{cff4KD2ZYzkArq=;H(Xd)1CAd%byUXZdBHcI*%a24Zj{Hm@XA}wj$=7~$Q*>&4} z2-V62ek{rKhPvvB711`qtAy+q{f1yWuFDcYt}hP)Vd>G?;VTb^P4 z(QDa?zvetCoB_)iGdmQ4VbG@QQ5Zt9a&t(D5Rf#|hC`LrONeUkbV)QF`ySE5x+t_v z-(cW{S13ye9>gtJm6w&>WwJynxJQm8U2My?#>+(|)JK}bEufIYSI5Y}T;vs?rzmLE zAIk%;^qbd@9WUMi*cGCr=oe1-nthYRQlhVHqf{ylD^0S09pI}qOQO=3&dBsD)BWo# z$NE2Ix&L&4|Aj{;ed*A?4z4S!7o_Kg^8@%#ZW26_F<>y4ghZ0b|3+unIoWDUVfen~ z`4`-cD7qxQSm9hF-;6WvCbu$t5r$LCOh}=`k1(W<&bG-xK{VXFl-cD%^Q*x-9eq;k8FzxAqZB zH@ja_3%O7XF~>owf3LSC_Yn!iO}|1Uc5uN{Wr-2lS=7&JlsYSp3IA%=E?H6JNf()z zh>jA>JVsH}VC>3Be>^UXk&3o&rK?eYHgLwE-qCHNJyzDLmg4G(uOFX5g1f(C{>W3u zn~j`zexZ=sawG8W+|SErqc?uEvQP(YT(YF;u%%6r00FP;yQeH)M9l+1Sv^yddvGo- z%>u>5SYyJ|#8_j&%h3#auTJ!4y@yEg<(wp#(~NH zXP7B#sv@cW{D4Iz1&H@5wW(F82?-JmcBt@Gw1}WK+>FRXnX(8vwSeUw{3i%HX6-pvQS-~Omm#x-udgp{=9#!>kDiLwqs_7fYy{H z)jx_^CY?5l9#fR$wukoI>4aETnU>n<$UY!JDlIvEti908)Cl2Ziyjjtv|P&&_8di> z<^amHu|WgwMBKHNZ)t)AHII#SqDIGTAd<(I0Q_LNPk*?UmK>C5=rIN^gs}@65VR*!J{W;wp5|&aF8605*l-Sj zQk+C#V<#;=Sl-)hzre6n0n{}|F=(#JF)X4I4MPhtm~qKeR8qM?a@h!-kKDyUaDrqO z1xstrCRCmDvdIFOQ7I4qesby8`-5Y>t_E1tUTVOPuNA1De9| z8{B0NBp*X2-ons_BNzb*Jk{cAJ(^F}skK~i;p0V(R7PKEV3bB;syZ4(hOw47M*-r8 z3qtuleeteUl$FHL$)LN|q8&e;QUN4(id`Br{rtsjpBdriO}WHLcr<;aqGyJP{&d6? zMKuMeLbc=2X0Q_qvSbl3r?F8A^oWw9Z{5@uQ`ySGm@DUZ=XJ^mKZ-ipJtmiXjcu<%z?Nj%-1QY*O{NfHd z=V}Y(UnK=f?xLb-_~H1b2T&0%O*2Z3bBDf06-nO*q%6uEaLs;=omaux7nqqW%tP$i zoF-PC%pxc(ymH{^MR_aV{@fN@0D1g&zv`1$Pyu3cvdR~(r*3Y%DJ@&EU?EserVEJ` zEprux{EfT+(Uq1m4F?S!TrZ+!AssSdX)fyhyPW6C`}ko~@y#7acRviE(4>moNe$HXzf zY@@fJa~o_r5nTeZ7ceiXI=k=ISkdp1gd1p)J;SlRn^5;rog!MlTr<<6-U9|oboRBN zlG~o*dR;%?9+2=g==&ZK;Cy0pyQFe)x!I!8g6;hGl`{{3q1_UzZy)J@c{lBIEJVZ& z!;q{8h*zI!kzY#RO8z3TNlN$}l;qj10=}du!tIKJs8O+?KMJDoZ+y)Iu`x`yJ@krO zwxETN$i!bz8{!>BKqHpPha{96eriM?mST)_9Aw-1X^7&;Bf=c^?17k)5&s08^E$m^ zRt02U_r!99xfiow-XC~Eo|Yt8t>32z=rv$Z;Ps|^26H73JS1Xle?;-nisDq$K5G3y znR|l8@rlvv^wj%tdgw+}@F#Ju{SkrQdqZ?5zh;}|IPIdhy3ivi0Q41C@4934naAaY z%+otS8%Muvrr{S-Y96G?b2j0ldu1&coOqsq^vfcUT3}#+=#;fii6@M+hDp}dr9A0Y zjbhvqmB03%4jhsZ{_KQfGh5HKm-=dFxN;3tnwBej^uzcVLrrs z>eFP-jb#~LE$qTP9JJ;#$nVOw%&;}y>ezA6&i8S^7YK#w&t4!A36Ub|or)MJT z^GGrzgcnQf6D+!rtfuX|Pna`Kq*ScO#H=de2B7%;t+Ij<>N5@(Psw%>nT4cW338WJ z>TNgQ^!285hS1JoHJcBk;3I8%#(jBmcpEkHkQDk%!4ygr;Q2a%0T==W zT#dDH>hxQx2E8+jE~jFY$FligkN&{vUZeIn*#I_Ca!l&;yf){eghi z>&?fXc-C$z8ab$IYS`7g!2#!3F@!)cUquAGR2oiR0~1pO<$3Y$B_@S2dFwu~B0e4D z6(WiE@O{(!vP<(t{p|S5#r$jl6h;3@+ygrPg|bBDjKgil!@Sq)5;rXNjv#2)N5_nn zuqEURL>(itBYrT&3mu-|q;soBd52?jMT75cvXYR!uFuVP`QMot+Yq?CO%D9$Jv24r zhq1Q5`FD$r9%&}9VlYcqNiw2#=3dZsho0cKKkv$%X&gmVuv&S__zyz@0zmZdZI59~s)1xFs~kZS0C^271hR*O z9nt$5=y0gjEI#S-iV0paHx!|MUNUq&$*zi>DGt<#?;y;Gms|dS{2#wF-S`G3$^$7g z1#@7C65g$=4Ij?|Oz?X4=zF=QfixmicIw{0oDL5N7iY}Q-vcVXdyQNMb>o_?3A?e6 z$4`S_=6ZUf&KbMgpn6Zt>6n~)zxI1>{HSge3uKBiN$01WB9OXscO?jd!)`?y5#%yp zJvgJU0h+|^MdA{!g@E=dJuyHPOh}i&alC+cY*I3rjB<~DgE{`p(FdHuXW;p$a+%5` zo{}x#Ex3{Sp-PPi)N8jGVo{K!$^;z%tVWm?b^oG8M?Djk)L)c{_-`@F|8LNu|BTUp zQY6QJVzVg8S{8{Pe&o}Ux=ITQ6d42;0l}OSEA&Oci$p?-BL187L6rJ>Q)aX0)Wf%T zneJF2;<-V%-VlcA?X03zpf;wI&8z9@Hy0BZm&ac-Gdtgo>}VkZYk##OOD+nVOKLFJ z5hgXAhkIzZtCU%2M#xl=D7EQPwh?^gZ_@0p$HLd*tF>qgA_P*dP;l^cWm&iQSPJZE zBoipodanrwD0}}{H#5o&PpQpCh61auqlckZq2_Eg__8;G-CwyH#h1r0iyD#Hd_$WgM89n+ldz;=b!@pvr4;x zs|YH}rQuCyZO!FWMy%lUyDE*0)(HR}QEYxIXFexCkq7SHmSUQ)2tZM2s`G<9dq;Vc ziNVj5hiDyqET?chgEA*YBzfzYh_RX#0MeD@xco%)ON%6B7E3#3iFBkPK^P_=&8$pf zpM<0>QmE~1FX1>mztm>JkRoosOq8cdJ1gF5?%*zMDak%qubN}SM!dW6fgH<*F>4M7 zX}%^g{>ng^2_xRNGi^a(epr8SPSP>@rg7s=0PO-#5*s}VOH~4GpK9<4;g=+zuJY!& ze_ld=ybcca?dUI-qyq2Mwl~-N%iCGL;LrE<#N}DRbGow7@5wMf&d`kT-m-@geUI&U z0NckZmgse~(#gx;tsChgNd|i1Cz$quL>qLzEO}ndg&Pg4f zy`?VSk9X5&Ab_TyKe=oiIiuNTWCsk6s9Ie2UYyg1y|i}B7h0k2X#YY0CZ;B7!dDg7 z_a#pK*I7#9-$#Iev5BpN@xMq@mx@TH@SoNWc5dv%^8!V}nADI&0K#xu_#y)k%P2m~ zqNqQ{(fj6X8JqMe5%;>MIkUDd#n@J9Dm~7_wC^z-Tcqqnsfz54jPJ1*+^;SjJzJhG zIq!F`Io}+fRD>h#wjL;g+w?Wg`%BZ{f()%Zj)sG8permeL0eQ9vzqcRLyZ?IplqMg zpQaxM11^`|6%3hUE9AiM5V)zWpPJ7nt*^FDga?ZP!U1v1aeYrV2Br|l`J^tgLm;~%gX^2l-L9L`B?UDHE9_+jaMxy|dzBY4 zjsR2rcZ6HbuyyXsDV(K0#%uPd#<^V%@9c7{6Qd_kQEZL&;z_Jf+eabr)NF%@Ulz_a1e(qWqJC$tTC! zwF&P-+~VN1Vt9OPf`H2N{6L@UF@=g+xCC_^^DZ`8jURfhR_yFD7#VFmklCR*&qk;A zzyw8IH~jFm+zGWHM5|EyBI>n3?2vq3W?aKt8bC+K1`YjklQx4*>$GezfU%E|>Or9Y zNRJ@s(>L{WBXdNiJiL|^In*1VA`xiE#D)%V+C;KuoQi{1t3~4*8 z;tbUGJ2@2@$XB?1!U;)MxQ}r67D&C49k{ceku^9NyFuSgc}DC2pD|+S=qLH&L}Vd4 zM=-UK4{?L?xzB@v;qCy}Ib65*jCWUh(FVc&rg|+KnopG`%cb>t;RNv=1%4= z#)@CB7i~$$JDM>q@4ll8{Ja5Rsq0 z$^|nRac)f7oZH^=-VdQldC~E_=5%JRZSm!z8TJocv`w<_e0>^teZ1en^x!yQse%Lf z;JA5?0vUIso|MS03y${dX19A&bU4wXS~*T7h+*4cgSIX11EB?XGiBS39hvWWuyP{!5AY^x5j{!c?z<}7f-kz27%b>llPq%Z7hq+CU|Ev2 z*jh(wt-^7oL`DQ~Zw+GMH}V*ndCc~ zr>WVQHJQ8ZqF^A7sH{N5~PbeDihT$;tUP`OwWn=j6@L+!=T|+ze%YQ zO+|c}I)o_F!T(^YLygYOTxz&PYDh9DDiv_|Ewm~i7|&Ck^$jsv_0n_}q-U5|_1>*L44)nt!W|;4q?n&k#;c4wpSx5atrznZbPc;uQI^I}4h5Fy`9J)l z7yYa7Rg~f@0oMHO;seQl|E@~fd|532lLG#e6n#vXrfdh~?NP){lZ z&3-33d;bUTEAG=!4_{YHd3%GCV=WS|2b)vZgX{JC)?rsljjzWw@Hflbwg3kIs^l%y zm3fVP-55Btz;<-p`X(ohmi@3qgdHmwXfu=gExL!S^ve^MsimP zNCBV>2>=BjLTobY^67f;8mXQ1YbM_NA3R^s z{zhY+5@9iYKMS-)S>zSCQuFl!Sd-f@v%;;*fW5hme#xAvh0QPtJ##}b>&tth$)6!$ z0S&b2OV-SE<|4Vh^8rs*jN;v9aC}S2EiPKo(G&<6C|%$JQ{;JEg-L|Yob*<-`z?AsI(~U(P>cC=1V$OETG$7i# zG#^QwW|HZuf3|X|&86lOm+M+BE>UJJSSAAijknNp*eyLUq=Au z7&aqR(x8h|>`&^n%p#TPcC@8@PG% zM&7k6IT*o-NK61P1XGeq0?{8kA`x;#O+|7`GTcbmyWgf^JvWU8Y?^7hpe^85_VuRq7yS~8uZ=Cf%W^OfwF_cbBhr`TMw^MH0<{3y zU=y;22&oVlrH55eGNvoklhfPM`bPX`|C_q#*etS^O@5PeLk(-DrK`l|P*@#T4(kRZ z`AY7^%&{!mqa5}q%<=x1e29}KZ63=O>89Q)yO4G@0USgbGhR#r~OvWI4+yu4*F8o`f?EG~x zBCEND=ImLu2b(FDF3sOk_|LPL!wrzx_G-?&^EUof1C~A{feam{2&eAf@2GWem7! z|LV-lff1Dk+mvTw@=*8~0@_Xu@?5u?-u*r8E7>_l1JRMpi{9sZqYG+#Ty4%Mo$`ds zsVROZH*QoCErDeU7&=&-ma>IUM|i_Egxp4M^|%^I7ecXzq@K8_oz!}cHK#>&+$E4rs2H8Fyc)@Bva?(KO%+oc!+3G0&Rv1cP)e9u_Y|dXr#!J;n%T4+9rTF>^m_4X3 z(g+$G6Zb@RW*J-IO;HtWHvopoVCr7zm4*h{rX!>cglE`j&;l_m(FTa?hUpgv%LNV9 zkSnUu1TXF3=tX)^}kDZk|AF%7FmLv6sh?XCORzhTU%d>y4cC;4W5mn=i6vLf2 ztbTQ8RM@1gn|y$*jZa8&u?yTOlNo{coXPgc%s;_Y!VJw2Z1bf%57p%kC1*5e{bepl zwm?2YGk~x=#69_Ul8A~(BB}>UP27=M)#aKrxWc-)rLL+97=>x|?}j)_5ewvoAY?P| z{ekQQbmjbGC%E$X*x-M=;Fx}oLHbzyu=Dw>&WtypMHnOc92LSDJ~PL7sU!}sZw`MY z&3jd_wS8>a!si2Y=ijCo(rMnAqq z-o2uzz}Fd5wD%MAMD*Y&=Ct?|B6!f0jfiJt;hvkIyO8me(u=fv_;C;O4X^vbO}R_% zo&Hx7C@EcZ!r%oy}|S-8CvPR?Ns0$j`FtMB;h z`#0Qq)+6Fxx;RCVnhwp`%>0H4hk(>Kd!(Y}>U+Tr_6Yp?W%jt_zdusOcA$pTA z(4l9$K=VXT2ITDs!OcShuUlG=R6#x@t74B2x7Dle%LGwsZrtiqtTuZGFUio_Xwpl} z=T7jdfT~ld#U${?)B67E*mP*E)XebDuMO(=3~Y=}Z}rm;*4f~7ka196QIHj;JK%DU z?AQw4I4ZufG}gmfVQ3w{snkpkgU~Xi;}V~S5j~;No^-9eZEYvA`Et=Q4(5@qcK=Pr zk9mo>v!%S>YD^GQc7t4c!C4*qU76b}r(hJhO*m-s9OcsktiXY#O1<OoH z#J^Y@1A;nRrrxNFh?3t@Hx9d>EZK*kMb-oe`2J!gZ;~I*QJ*f1p93>$lU|4qz!_zH z&mOaj#(^uiFf{*Nq?_4&9ZssrZeCgj1J$1VKn`j+bH%9#C5Q5Z@9LYX1mlm^+jkHf z+CgcdXlX5);Ztq6OT@;UK_zG(M5sv%I`d2(i1)>O`VD|d1_l(_aH(h>c7fP_$LA@d z6Wgm))NkU!v^YaRK_IjQy-_+>f_y(LeS@z+B$5be|FzXqqg}`{eYpO;sXLrU{*fJT zQHUEXoWk%wh%Kal`E~jiu@(Q@&d&dW*!~9;T=gA{{~NJwQvULf;s43Ku#A$NgaR^1 z%U3BNX`J^YE-#2dM*Ov*CzGdP9^`iI&`tmD~Bwqy4*N=DHt%RycykhF* zc7BcXG28Jvv(5G8@-?OATk6|l{Rg1 zwdU2Md1Qv?#$EO3E}zk&9>x1sQiD*sO0dGSUPkCN-gjuppdE*%*d*9tEWyQ%hRp*7 zT`N^=$PSaWD>f;h@$d2Ca7 z8bNsm14sdOS%FQhMn9yC83$ z-YATg3X!>lWbLUU7iNk-`O%W8MrgI03%}@6l$9+}1KJ1cTCiT3>^e}-cTP&aEJcUt zCTh_xG@Oa-v#t_UDKKfd#w0tJfA+Ash!0>X&`&;2%qv$!Gogr4*rfMcKfFl%@{ztA zwoAarl`DEU&W_DUcIq-{xaeRu(ktyQ64-uw?1S*A>7pRHH5_F)_yC+2o@+&APivkn zwxDBp%e=?P?3&tiVQb8pODI}tSU8cke~T#JLAxhyrZ(yx)>fUhig`c`%;#7Ot9le# zSaep4L&sRBd-n&>6=$R4#mU8>T>=pB)feU9;*@j2kyFHIvG`>hWYJ_yqv?Kk2XTw` z42;hd=hm4Iu0h{^M>-&c9zKPtqD>+c$~>k&Wvq#>%FjOyifO%RoFgh*XW$%Hz$y2-W!@W6+rFJja=pw-u_s0O3WMVgLb&CrCQ)8I^6g!iQj%a%#h z<~<0S#^NV4n!@tiKb!OZbkiSPp~31?f9Aj#fosfd*v}j6&7YpRGgQ5hI_eA2m+Je) zT2QkD;A@crBzA>7T zw4o1MZ_d$)puHvFA2J|`IwSXKZyI_iK_}FvkLDaFj^&6}e|5@mrHr^prr{fPVuN1+ z4=9}DkfKLYqUq7Q7@qa$)o6&2)kJx-3|go}k9HCI6ahL?NPA&khLUL}k_;mU&7GcN zNG6(xXW}(+a%IT80=-13-Q~sBo>$F2m`)7~wjW&XKndrz8soC*br=F*A_>Sh_Y}2Mt!#A1~2l?|hj) z9wpN&jISjW)?nl{@t`yuLviwvj)vyZQ4KR#mU-LE)mQ$yThO1oohRv;93oEXE8mYE zXPQSVCK~Lp3hIA_46A{8DdA+rguh@98p?VG2+Nw(4mu=W(sK<#S`IoS9nwuOM}C0) zH9U|6N=BXf!jJ#o;z#6vi=Y3NU5XT>ZNGe^z4u$i&x4ty^Sl;t_#`|^hmur~;r;o- z*CqJb?KWBoT`4`St5}10d*RL?!hm`GaFyxLMJPgbBvjVD??f7GU9*o?4!>NabqqR! z{BGK7%_}96G95B299eErE5_rkGmSWKP~590$HXvsRGJN5-%6d@=~Rs_68BLA1RkZb zD%ccBqGF0oGuZ?jbulkt!M}{S1;9gwAVkgdilT^_AS`w6?UH5Jd=wTUA-d$_O0DuM z|9E9XZFl$tZctd`Bq=OfI(cw4A)|t zl$W~3_RkP zFA6wSu+^efs79KH@)0~c3Dn1nSkNj_s)qBUGs6q?G0vjT&C5Y3ax-seA_+_}m`aj} zvW04)0TSIpqQkD@#NXZBg9z@GK1^ru*aKLrc4{J0PjhNfJT}J;vEeJ1ov?*KVNBy< zXtNIY3TqLZ=o1Byc^wL!1L6#i6n(088T9W<_iu~$S&VWGfmD|wNj?Q?Dnc#6iskoG zt^u26JqFnt=xjS-=|ACC%(=YQh{_alLW1tk;+tz1ujzeQ--lEu)W^Jk>UmHK(H303f}P2i zrsrQ*nEz`&{V!%2O446^8qLR~-Pl;2Y==NYj^B*j1vD}R5plk>%)GZSSjbi|tx>YM zVd@IS7b>&Uy%v==*35wGwIK4^iV{31mc)dS^LnN8j%#M}s%B@$=bPFI_ifcyPd4hilEWm71chIwfIR(-SeQaf20{;EF*(K(Eo+hu{}I zZkjXyF}{(x@Ql~*yig5lAq7%>-O5E++KSzEe(sqiqf1>{Em)pN`wf~WW1PntPpzKX zn;14G3FK7IQf!~n>Y=cd?=jhAw1+bwlVcY_kVuRyf!rSFNmR4fOc(g7(fR{ANvcO< zbG|cnYvKLa>dU(Z9YP796`Au?gz)Ys?w!af`F}1#W>x_O|k9Q z>#<6bKDt3Y}?KT2tmhU>H6Umn}J5M zarILVggiZs=kschc2TKib2`gl^9f|(37W93>80keUkrC3ok1q{;PO6HMbm{cZ^ROcT#tWWsQy?8qKWt<42BGryC(Dx>^ohIa0u7$^)V@Bn17^(VUgBD> zAr*Wl6UwQ&AAP%YZ;q2cZ;@2M(QeYFtW@PZ+mOO5gD1v-JzyE3^zceyE5H?WLW?$4 zhBP*+3i<09M$#XU;jwi7>}kW~v%9agMDM_V1$WlMV|U-Ldmr|<_nz*F_kcgrJnrViguEnJt{=Mk5f4Foin7(3vUXC>4gyJ>sK<;-p{h7 z2_mr&Fca!E^7R6VvodGznqJn3o)Ibd`gk>uKF7aemX*b~Sn#=NYl5j?v*T4FWZF2D zaX(M9hJ2YuEi%b~4?RkJwT*?aCRT@ecBkq$O!i}EJJEw`*++J_a>gsMo0CG^pZ3x+ zdfTSbCgRwtvAhL$p=iIf7%Vyb!j*UJsmOMler--IauWQ;(ddOk+U$WgN-RBle~v9v z9m2~@h|x*3t@m+4{U2}fKzRoVePrF-}U{`YT|vW?~64Bv*7|Dz03 zRYM^Yquhf*ZqkN?+NK4Ffm1;6BR0ZyW3MOFuV1ljP~V(=-tr^Tgu#7$`}nSd<8?cP z`VKtIz5$~InI0YnxAmn|pJZj+nPlI3zWsykXTKRnDCBm~Dy*m^^qTuY+8dSl@>&B8~0H$Y0Zc25APo|?R= z>_#h^kcfs#ae|iNe{BWA7K1mLuM%K!_V?fDyEqLkkT&<`SkEJ;E+Py^%hPVZ(%a2P4vL=vglF|X_`Z$^}q470V+7I4;UYdcZ7vU=41dd{d#KmI+|ZGa>C10g6w1a?wxAc&?iYsEv zuCwWvcw4FoG=Xrq=JNyPG*yIT@xbOeV`$s_kx`pH0DXPf0S7L?F208x4ET~j;yQ2c zhtq=S{T%82U7GxlUUKMf-NiuhHD$5*x{6}}_eZ8_kh}(}BxSPS9<(x2m$Rn0sx>)a zt$+qLRJU}0)5X>PXVxE?Jxpw(kD0W43ctKkj8DjpYq}lFZE98Je+v2t7uxuKV;p0l z5b9smYi5~k2%4aZe+~6HyobTQ@4_z#*lRHl# zSA`s~Jl@RGq=B3SNQF$+puBQv>DaQ--V!alvRSI~ZoOJx3VP4sbk!NdgMNBVbG&BX zdG*@)^g4#M#qoT`^NTR538vx~rdyOZcfzd7GBHl68-rG|fkofiGAXTJx~`~%a&boY zZ#M4sYwHIOnu-Mr!Ltpl8!NrX^p74tq{f_F4%M@&<=le;>xc5pAi&qn4P>04D$fp` z(OuJXQia--?vD0DIE6?HC|+DjH-?Cl|GqRKvs8PSe027_NH=}+8km9Ur8(JrVx@*x z0lHuHd=7*O+&AU_B;k{>hRvV}^Uxl^L1-c-2j4V^TG?2v66BRxd~&-GMfcvKhWgwu z60u{2)M{ZS)r*=&J4%z*rtqs2syPiOQq(`V0UZF)boPOql@E0U39>d>MP=BqFeJzz zh?HDKtY3%mR~reR7S2rsR0aDMA^a|L^_*8XM9KjabpYSBu z;zkfzU~12|X_W_*VNA=e^%Za14PMOC!z`5Xt|Fl$2bP9fz>(|&VJFZ9{z;;eEGhOl zl7OqqDJzvgZvaWc7Nr!5lfl*Qy7_-fy9%f(v#t#&2#9o-ba%J3(%s#C=@dagx*I{d zB&AzGT9EEiknWJU^naNdz7Logo%#OFV!eyCIQuzgpZDDN-1F}JJTdGXiLN85p|GT! zGOfNd8^RD;MsK*^3gatg2#W0J<8j)UCkUYoZRR|R*UibOm-G)S#|(`$hPA7UmH+fT ziZxTgeiR_yzvNS1s+T!xw)QgNSH(_?B@O?uTBwMj`G)2c^8%g8zu zxMu5SrQ^J+K91tkPrP%*nTpyZor#4`)}(T-Y8eLd(|sv8xcIoHnicKyAlQfm1YPyI z!$zimjMlEcmJu?M6z|RtdouAN1U5lKmEWY3gajkPuUHYRvTVeM05CE@`@VZ%dNoZN z>=Y3~f$~Gosud$AN{}!DwV<6CHm3TPU^qcR!_0$cY#S5a+GJU-2I2Dv;ktonSLRRH zALlc(lvX9rm-b5`09uNu904c}sU(hlJZMp@%nvkcgwkT;Kd7-=Z_z9rYH@8V6Assf zKpXju&hT<=x4+tCZ{elYtH+_F$V=tq@-`oC%vdO>0Wmu#w*&?_=LEWRJpW|spYc8V z=$)u#r}Pu7kvjSuM{FSyy9_&851CO^B zTm$`pF+lBWU!q>X#;AO1&=tOt=i!=9BVPC#kPJU}K$pO&8Ads)XOFr336_Iyn z$d{MTGYQLX9;@mdO;_%2Ayw3hv}_$UT00*e{hWxS?r=KT^ymEwBo429b5i}LFmSk` zo)-*bF1g;y@&o=34TW|6jCjUx{55EH&DZ?7wB_EmUg*B4zc6l7x-}qYLQR@^7o6rrgkoujRNym9O)K>wNfvY+uy+4Om{XgRHi#Hpg*bZ36_X%pP`m7FIF z?n?G*g&>kt$>J_PiXIDzgw3IupL3QZbysSzP&}?JQ-6TN-aEYbA$X>=(Zm}0{hm6J zJnqQnEFCZGmT06LAdJ^T#o`&)CA*eIYu?zzDJi#c$1H9zX}hdATSA|zX0Vb^q$mgg z&6kAJ=~gIARct>}4z&kzWWvaD9#1WK=P>A_aQxe#+4cpJtcRvd)TCu! z>eqrt)r(`qYw6JPKRXSU#;zYNB7a@MYoGuAT0Nzxr`>$=vk`uEq2t@k9?jYqg)MXl z67MA3^5_}Ig*mycsGeH0_VtK3bNo;8#0fFQ&qDAj=;lMU9%G)&HL>NO|lWU3z+m4t7 zfV*3gSuZ++rIWsinX@QaT>dsbD>Xp8%8c`HLamm~(i{7L&S0uZ;`W-tqU4XAgQclM$PxE76OH(PSjHjR$(nh({vsNnawhP!!HcP!l)5 zG;C=k0xL<^q+4rpbp{sGzcc~ZfGv9J*k~PPl}e~t$>WPSxzi0}05(D6d<=5+E}Y4e z@_QZtDcC7qh4#dQFYb6Pulf_8iAYYE z1SWJfNe5@auBbE5O=oeO@o*H5mS(pm%$!5yz-71~lEN5=x0eN|V`xAeP;eTje?eC= z53WneK;6n35{OaIH2Oh6Hx)kV-jL-wMzFlynGI8Wk_A<~_|06rKB#Pi_QY2XtIGW_ zYr)RECK_JRzR1tMd(pM(L=F98y~7wd4QBKAmFF(AF(e~+80$GLZpFc;a{kj1h}g4l z3SxIRlV=h%Pl1yRacl^g>9q%>U+`P(J`oh-w8i82mFCn|NJ5oX*^VKODX2>~HLUky z3D(ak0Sj=Kv^&8dUhU(3Ab!U5TIy97PKQ))&`Ml~hik%cHNspUpCn24cqH@dq6ZVo zO9xz!cEMm;NL;#z-tThlFF%=^ukE8S0;hDMR_`rv#eTYg7io1w9n_vJpK+6%=c#Y?wjAs_(#RQA0gr&Va2BQTq` zUc8)wHEDl&Uyo<>-PHksM;b-y(`E_t8Rez@Iw+eogcEI*FDg@Bc;;?3j3&kPsq(mx z+Yr_J#?G6D?t2G%O9o&e7Gbf&>#(-)|8)GIbG_a${TU26cVrIQSt=% zQ~XY-b1VQVc>IV=7um0^Li>dF z`zSm_o*i@ra4B+Tw5jdguVqx`O(f4?_USIMJzLvS$*kvBfEuToq-VR%K*%1VHu=++ zQ`=cG3cCnEv{ZbP-h9qbkF}%qT$j|Z7ZB2?s7nK@gM{bAD=eoDKCCMlm4LG~yre!- zzPP#Rn9ZDUgb4++M78-V&VX<1ah(DN z(4O5b`Fif%*k?L|t%!WY`W$C_C`tzC`tI7XC`->oJs_Ezs=K*O_{*#SgNcvYdmBbG zHd8!UTzGApZC}n7LUp1fe0L<3|B5GdLbxX@{ETeUB2vymJgWP0q2E<&!Dtg4>v`aa zw(QcLoA&eK{6?Rb&6P0kY+YszBLXK49i~F!jr)7|xcnA*mOe1aZgkdmt4{Nq2!!SL z`aD{6M>c00muqJt4$P+RAj*cV^vn99UtJ*s${&agQ;C>;SEM|l%KoH_^kAcmX=%)* zHpByMU_F12iGE#68rHGAHO_ReJ#<2ijo|T7`{PSG)V-bKw}mpTJwtCl%cq2zxB__m zM_p2k8pDmwA*$v@cmm>I)TW|7a7ng*X7afyR1dcuVGl|BQzy$MM+zD{d~n#)9?1qW zdk(th4Ljb-vpv5VUt&9iuQBnQ$JicZ)+HoL`&)B^Jr9F1wvf=*1and~v}3u{+7u7F zf0U`l4Qx-ANfaB3bD1uIeT^zeXerps8nIW(tmIxYSL;5~!&&ZOLVug2j4t7G=zzK+ zmPy5<4h%vq$Fw)i1)ya{D;GyEm3fybsc8$=$`y^bRdmO{XU#95EZ$I$bBg)FW#=}s z@@&c?xwLF3|C7$%>}T7xl0toBc6N^C{!>a8vWc=G!bAFKmn{AKS6RxOWIJBZXP&0CyXAiHd?7R#S46K6UXYXl#c_#APL5SfW<<-|rcfX&B6e*isa|L^RK=0}D`4q-T0VAs0 zToyrF6`_k$UFGAGhY^&gg)(Fq0p%J{h?E)WQ(h@Gy=f6oxUSAuT4ir}jI)36|NnmnI|vtij;t!jT?6Jf-E19}9Lf9(+N+ z)+0)I5mST_?3diP*n2=ZONTYdXkjKsZ%E$jjU@0w_lL+UHJOz|K{{Uh%Zy0dhiqyh zofWXzgRyFzY>zpMC8-L^43>u#+-zlaTMOS(uS!p{Jw#u3_9s)(s)L6j-+`M5sq?f+ zIIcjq$}~j9b`0_hIz~?4?b(Sqdpi(;1=8~wkIABU+APWQdf5v@g=1c{c{d*J(X5+cfEdG?qxq z{GKkF;)8^H&Xdi~fb~hwtJRsfg#tdExEuDRY^x9l6=E+|fxczIW4Z29NS~-oLa$Iq z93;5$(M0N8ba%8&q>vFc=1}a8T?P~_nrL5tYe~X>G=3QoFlBae8vVt-K!^@vusN<8gQJ!WD7H%{*YgY0#(tXxXy##C@o^U7ysxe zLmUWN@4)JBjjZ3G-_)mrA`|NPCc8Oe!%Ios4$HWpBmJse7q?)@Xk%$x&lIY>vX$7L zpfNWlXxy2p7TqW`Wq22}Q3OC2OWTP_X(*#kRx1WPe%}$C!Qn^FvdYmvqgk>^nyk;6 zXv*S#P~NVx1n6pdbXuX9x_}h1SY#3ZyvLZ&VnWVva4)9D|i7kjGY{>am&^ z-_x1UYM1RU#z17=AruK~{BK$A65Sajj_OW|cpYQBGWO*xfGJXSn4E&VMWchq%>0yP z{M2q=zx!VnO71gb8}Al2i+uxb=ffIyx@oso@8Jb88ld6M#wgXd=WcX$q$91o(94Ek zjeBqQ+CZ64hI>sZ@#tjdL}JeJu?GS7N^s$WCIzO`cvj60*d&#&-BQ>+qK#7l+!u1t zBuyL-Cqups?2>)ek2Z|QnAqs_`u1#y8=~Hvsn^2Jtx-O`limc*w;byk^2D-!*zqRi zVcX+4lzwcCgb+(lROWJ~qi;q2!t6;?%qjGcIza=C6{T7q6_?A@qrK#+)+?drrs3U}4Fov+Y}`>M z#40OUPpwpaC-8&q8yW0XWGw`RcSpBX+7hZ@xarfCNnrl-{k@`@Vv> zYWB*T=4hLJ1SObSF_)2AaX*g(#(88~bVG9w)ZE91eIQWflNecYC zzUt}ov<&)S&i$}?LlbIi9i&-g=UUgjWTq*v$!0$;8u&hwL*S^V!GPSpM3PR3Ra5*d z7d77UC4M{#587NcZS4+JN=m#i)7T0`jWQ{HK3rIIlr3cDFt4odV25yu9H1!}BVW-& zrqM5DjDzbd^pE^Q<-$1^_tX)dX8;97ILK{ z!{kF{!h`(`6__+1UD5=8sS&#!R>*KqN9_?(Z$4cY#B)pG8>2pZqI;RiYW6aUt7kk*s^D~Rml_fg$m+4+O5?J&p1)wE zp5L-X(6og1s(?d7X#l-RWO+5Jj(pAS{nz1abM^O;8hb^X4pC7ADpzUlS{F~RUoZp^ zuJCU_fq}V!9;knx^uYD2S9E`RnEsyF^ZO$;`8uWNI%hZzKq=t`q12cKEvQjJ9dww9 zCerpM3n@Ag+XZJztlqHRs!9X(Dv&P;_}zz$N&xwA@~Kfnd3}YiABK*T)Ar2E?OG6V z<;mFs`D?U7>Rradv7(?3oCZZS_0Xr#3NNkpM1@qn-X$;aNLYL;yIMX4uubh^Xb?HloImt$=^s8vm)3g!{H1D|k zmbg_Rr-ypQokGREIcG<8u(=W^+oxelI&t0U`dT=bBMe1fl+9!l&vEPFFu~yAu!XIv4@S{;| z8?%<1@hJp%7AfZPYRARF1hf`cq_VFQ-y74;EdMob{z&qec2hiQJOQa>f-?Iz^VXOr z-wnfu*uT$(5WmLsGsVkHULPBvTRy0H(}S0SQ18W0kp_U}8Phc3gz!Hj#*VYh$AiDE245!YA0M$Q@rM zT;}1DQ}MxV<)*j{hknSHyihgMPCK=H)b-iz9N~KT%<&Qmjf39L@&7b;;>9nQkDax- zk%7ZMA%o41l#(G5K=k{D{80E@P|I;aufYpOlIJXv!dS+T^plIVpPeZ)Gp`vo+?BWt z8U8u=C51u%>yDCWt>`VGkE5~2dD4y_8+n_+I9mFN(4jHJ&x!+l*>%}b4Z>z#(tb~< z+<+X~GIi`sDb=SI-7m>*krlqE3aQD?D5WiYX;#8m|ENYKw}H^95u!=n=xr3jxhCB&InJ7>zgLJg;i?Sjjd`YW!2; z%+y=LwB+MMnSGF@iu#I%!mvt)aXzQ*NW$cHNHwjoaLtqKCHqB}LW^ozBX?`D4&h%# zeMZ3ZumBn}5y9&odo3=hN$Q&SRte*^-SNZg2<}6>OzRpF91oy0{RuZU(Q0I zvx%|9>;)-Ca9#L)HQt~axu0q{745Ac;s1XQKV ze3D9I5gV5SP-J>&3U!lg1`HN>n5B6XxYpwhL^t0Z)4$`YK93vTd^7BD%<)cIm|4e!;*%9}B-3NX+J*Nr@;5(27Zmf(TmfHsej^Bz+J1 zXKIjJ)H{thL4WOuro|6&aPw=-JW8G=2 z|L4YL)^rYf7J7DOKXpTX$4$Y{-2B!jT4y^w8yh3LKRKO3-4DOshFk}N^^Q{r(0K0+ z?7w}x>(s{Diq6K)8sy)>%*g&{u>)l+-Lg~=gteW?pE`B@FE`N!F-+aE;XhjF+2|RV z8vV2((yeA-VDO;3=^E;fhW~b=Wd5r8otQrO{Vu)M1{j(+?+^q%xpYCojc6rmQ<&ytZ2ly?bw*X)WB8(n^B4Gmxr^1bQ&=m;I4O$g{ z3m|M{tmkOyAPnMHu(Z}Q1X1GM|A+)VDP3Fz934zSl)z>N|D^`G-+>Mej|VcK+?iew zQ3=DH4zz;i>z{Yv_l@j*?{936kxM{c7eK$1cf8wxL>>O#`+vsu*KR)te$adfTD*w( zAStXnZk<6N3V-Vs#GB%vXZat+(EFWbkbky#{yGY`rOvN)?{5qUuFv=r=dyYZrULf%MppWuNRUWc z8|YaIn}P0DGkwSZ(njAO$Zhr3Yw`3O1A+&F*2UjO{0`P%kK(qL;kEkfjRC=lxPRjL z{{4PO3-*5RZ_B3LUB&?ZpJ4nk1E4L&eT~HX0Jo(|uGQCW3utB@p)rF@W*n$==TlS zKiTfzhrLbAeRqru%D;fUwXOUcHud{pw@Ib1xxQ}<2)?KC&%y5PVef<7rcu2l!8dsy z?lvdaHJ#s$0m18y{x#fB$o=l)-sV?Qya5GWf#8Vd{~Grn@qgX#!EI`Y>++l%1A;eL z{_7t6jMeEr@a+oxyCL^+_}9Qc;i0&Xd%LXp?to*R|26LKHG(m0)*QF4*h;5%YG5<9)c> z1vq!7bIJSv1^27i-mcH!zX>ep3Iw0^{nx<1jOy)N_UoFD8v}x~2mEWapI3m~kMQkR z#&@4FuEGBn`mgtSx6jeY7vUQNf=^}sTZErIEpH!cy|@7Z zU4h_Oxxd2s=f{}$XXy4}%JqTSjRC cluster, final String bucketName, final String scopeName) { - this(cluster, bucketName, scopeName, null); - } - - private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, final String scopeName, - final CouchbaseStuffHandle transactionalOperator) { this.cluster = cluster; this.bucket = cluster.get().bucket(bucketName); this.scope = scopeName == null ? bucket.defaultScope() : bucket.scope(scopeName); this.exceptionTranslator = new CouchbaseExceptionTranslator(); - //this.transactionalOperator = transactionalOperator; } @Override @@ -142,23 +129,23 @@ public PersistenceExceptionTranslator getExceptionTranslator() { @Override public ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, AttemptContextReactive atr) { - // can't we just use AttemptContextReactive everywhere? Instead of creating AttemptContext(atr), then + // can't we just use AttemptContextReactive everywhere? Instead of creating AttemptContext(atr), then // accessing at.getACR() ? - AttemptContext at = AttemptContextReactiveAccessor.from( atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive())); + AttemptContext at = AttemptContextReactiveAccessor + .from(atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive())); return new ClientSessionImpl(this, transactions, config, at); } + // @Override + // public CouchbaseClientFactory with(CouchbaseStuffHandle txOp) { + // return new SimpleCouchbaseClientFactory(cluster, bucket.name(), scope.name(), txOp); + // } - //@Override - //public CouchbaseClientFactory with(CouchbaseStuffHandle txOp) { - // return new SimpleCouchbaseClientFactory(cluster, bucket.name(), scope.name(), txOp); - //} - - //@Override - //public CouchbaseStuffHandle getTransactionalOperator() { - // return (CouchbaseStuffHandle) transactionalOperator; - //} + // @Override + // public CouchbaseStuffHandle getTransactionalOperator() { + // return (CouchbaseStuffHandle) transactionalOperator; + // } @Override public void close() { diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index 9c1f5dda6..ee92d1588 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -377,7 +377,7 @@ public void replaceWithCasConflictResolvedViaRetry() { AtomicInteger tryCount = new AtomicInteger(0); Mono result = transactions.reactive(ctx -> { // get the ctx - // see TransactionalOperatorImpl.tranactional(). + // see TransactionalOperatorImpl.transactional(). ClientSession clientSession = couchbaseClientFactory .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, @@ -1009,12 +1009,6 @@ public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger System.err.println("declarativeFindReplacePersonCallback try: "+tryCount.incrementAndGet()); System.err.println("declarativeFindReplacePersonCallback cluster : "+callbackTm.template().getCouchbaseClientFactory().getCluster().block()); System.err.println("declarativeFindReplacePersonCallback resourceHolder : "+org.springframework.transaction.support.TransactionSynchronizationManager.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); - /* what are we trying to see here??? - TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { - System.err.println("declarativeFindReplacePersonCallback reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); - return Mono.just(sm); - }).block(); - */ Person p = personOperations.findById(Person.class).one(person.getId().toString()); return personOperations.replaceById(Person.class).one(p); } @@ -1045,7 +1039,7 @@ public Mono declarativeFindReplacePersonReactive(Person person, AtomicIn * @param person * @return */ - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) // doesn't retry public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); System.err.println("declarativeFindReplacePerson try: "+tryCount.incrementAndGet()); @@ -1054,7 +1048,7 @@ public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount } - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // doesn't retry public Mono declarativeSavePersonReactive(Person person) { assertInAnnotationTransaction(true); return personOperationsRx.insertById(Person.class).one(person); diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 05213718c..a7dd9416e 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -62,13 +62,8 @@ public abstract class ClusterAwareIntegrationTests { @BeforeAll static void setup(TestClusterConfig config) { testClusterConfig = config; - ClusterEnvironment env = config.seed() != null && config.seed().contains("cloud.couchbase.com") - ? ClusterEnvironment.builder() - .securityConfig(SecurityConfig.trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true)) - .build() - : ClusterEnvironment.builder().build(); - String connectString = config.seed() != null && config.seed().contains("cloud.couchbase.com") ? config.seed() - : connectionString(); + ClusterEnvironment env = ClusterEnvironment.builder().build(); + String connectString = connectionString(); try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectString, authenticator(), bucketName(), null, env)) { couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), CreatePrimaryQueryIndexOptions @@ -137,12 +132,8 @@ public static String connectionString() { protected static Set seedNodes() { return config().nodes().stream() .map(cfg -> SeedNode.create(cfg.hostname(), - Optional.ofNullable(config().seed() != null && config().seed().contains("cloud.couchbase.com") - ? cfg.ports().get(Services.KV_TLS) - : cfg.ports().get(Services.KV)), - Optional.ofNullable(config().seed() != null && config().seed().contains("cloud.couchbase.com") - ? cfg.ports().get(Services.MANAGER_TLS) - : cfg.ports().get(Services.MANAGER)))) + Optional.ofNullable(cfg.ports().get(Services.KV)), + Optional.ofNullable(cfg.ports().get(Services.MANAGER)))) .collect(Collectors.toSet()); } diff --git a/src/test/resources/integration.properties b/src/test/resources/integration.properties index f097d05bd..ed138ca00 100644 --- a/src/test/resources/integration.properties +++ b/src/test/resources/integration.properties @@ -2,7 +2,7 @@ # If set to false, it is assumed that the host is managing the cluster and # as a result no containers or anything will be spun up. # Options: containerized, mocked, unmanaged -cluster.type=mocked +cluster.type=unmanaged # Default configs for both cases cluster.adminUsername=Administrator cluster.adminPassword=password @@ -11,5 +11,5 @@ cluster.mocked.numNodes=1 cluster.mocked.numReplicas=1 # Entry point configuration if not managed # value of hostname and ns_server port -cluster.unmanaged.seed=127.0.0.1:8091 +cluster.unmanaged.seed=10.144.220.101:8091 cluster.unmanaged.numReplicas=0 From 95bf2fc371051c57f35aaddbf67f1c8b4f78795b Mon Sep 17 00:00:00 2001 From: Michael Reiche Date: Tue, 12 Apr 2022 10:06:29 -0700 Subject: [PATCH 4/9] Transaction Support. --- .../com/couchbase/client/java/Cluster.java | 578 ++++++++++++++++++ .../client/java/ClusterInterface.java | 107 ++++ .../CouchbaseTransactionInterceptor.java | 384 ++++++++++++ ...basePersonTransactionIntegrationTests.java | 152 +++-- .../util/CollectionAwareIntegrationTests.java | 2 +- 5 files changed, 1169 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/couchbase/client/java/Cluster.java create mode 100644 src/main/java/com/couchbase/client/java/ClusterInterface.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java diff --git a/src/main/java/com/couchbase/client/java/Cluster.java b/src/main/java/com/couchbase/client/java/Cluster.java new file mode 100644 index 000000000..907b58241 --- /dev/null +++ b/src/main/java/com/couchbase/client/java/Cluster.java @@ -0,0 +1,578 @@ +/* + * Copyright (c) 2018 Couchbase, Inc. + * + * 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 + * + * http://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 com.couchbase.client.java; + +import com.couchbase.client.core.Core; +import com.couchbase.client.core.diagnostics.ClusterState; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.diagnostics.PingResult; +import com.couchbase.client.core.env.Authenticator; +import com.couchbase.client.core.env.PasswordAuthenticator; +import com.couchbase.client.core.env.SeedNode; +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.core.error.TimeoutException; +import com.couchbase.client.core.msg.search.SearchRequest; +import com.couchbase.client.java.analytics.AnalyticsOptions; +import com.couchbase.client.java.analytics.AnalyticsResult; +import com.couchbase.client.java.diagnostics.DiagnosticsOptions; +import com.couchbase.client.java.diagnostics.PingOptions; +import com.couchbase.client.java.diagnostics.WaitUntilReadyOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.manager.analytics.AnalyticsIndexManager; +import com.couchbase.client.java.manager.bucket.BucketManager; +import com.couchbase.client.java.manager.eventing.EventingFunctionManager; +import com.couchbase.client.java.manager.query.QueryIndexManager; +import com.couchbase.client.java.manager.search.SearchIndexManager; +import com.couchbase.client.java.manager.user.UserManager; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.search.SearchOptions; +import com.couchbase.client.java.search.SearchQuery; +import com.couchbase.client.java.search.result.SearchResult; + +import java.time.Duration; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import static com.couchbase.client.core.util.Validators.notNull; +import static com.couchbase.client.core.util.Validators.notNullOrEmpty; +import static com.couchbase.client.java.AsyncCluster.extractClusterEnvironment; +import static com.couchbase.client.java.AsyncCluster.seedNodesFromConnectionString; +import static com.couchbase.client.java.AsyncUtils.block; +import static com.couchbase.client.java.ClusterOptions.clusterOptions; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_ANALYTICS_OPTIONS; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_DIAGNOSTICS_OPTIONS; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_QUERY_OPTIONS; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_SEARCH_OPTIONS; + +/** + * The {@link Cluster} is the main entry point when connecting to a Couchbase cluster. + *

    + * Most likely you want to start out by using the {@link #connect(String, String, String)} entry point. For more + * advanced options you want to use the {@link #connect(String, ClusterOptions)} method. The entry point that allows + * overriding the seed nodes ({@link #connect(Set, ClusterOptions)} is only needed if you run a couchbase cluster + * at non-standard ports. + *

    + * See the individual connect methods for more information, but here is a snippet to get you off the ground quickly. It + * assumes you have Couchbase running locally and the "travel-sample" sample bucket loaded: + *

    + * //Connect and open a bucket
    + * Cluster cluster = Cluster.connect("127.0.0.1","Administrator","password");
    + * Bucket bucket = cluster.bucket("travel-sample");
    + * Collection collection = bucket.defaultCollection();
    + *
    + * // Perform a N1QL query
    + * QueryResult queryResult = cluster.query("select * from `travel-sample` limit 5");
    + * System.out.println(queryResult.rowsAsObject());
    + *
    + * // Perform a KV request and load a document
    + * GetResult getResult = collection.get("airline_10");
    + * System.out.println(getResult);
    + * 
    + *

    + * When the application shuts down (or the SDK is not needed anymore), you are required to call {@link #disconnect()}. + * If you omit this step, the application will terminate (all spawned threads are daemon threads) but any operations + * or work in-flight will not be able to complete and lead to undesired side-effects. Note that disconnect will also + * shutdown all associated {@link Bucket buckets}. + *

    + * Cluster-level operations like {@link #query(String)} will not work unless at leas one bucket is opened against a + * pre 6.5 cluster. If you are using 6.5 or later, you can run cluster-level queries without opening a bucket. All + * of these operations are lazy, so the SDK will bootstrap in the background and service queries as quickly as possible. + * This also means that the first operations might be a bit slower until all sockets are opened in the background and + * the configuration is loaded. If you want to wait explicitly, you can utilize the {@link #waitUntilReady(Duration)} + * method before performing your first query. + *

    + * The SDK will only work against Couchbase Server 5.0 and later, because RBAC (role-based access control) is a first + * class concept since 3.0 and therefore required. + */ +public class Cluster implements ClusterInterface { + + /** + * Holds the underlying async cluster reference. + */ + private final AsyncCluster asyncCluster; + + /** + * Holds the adjacent reactive cluster reference. + */ + private final ReactiveCluster reactiveCluster; + + /** + * The search index manager manages search indexes. + */ + private final SearchIndexManager searchIndexManager; + + /** + * The user manager manages users and groups. + */ + private final UserManager userManager; + + /** + * The bucket manager manages buckets and allows to flush them. + */ + private final BucketManager bucketManager; + + /** + * Allows to manage query indexes. + */ + private final QueryIndexManager queryIndexManager; + + /** + * Allows to manage analytics indexes. + */ + private final AnalyticsIndexManager analyticsIndexManager; + + /** + * Allows to manage eventing functions. + */ + private final EventingFunctionManager eventingFunctionManager; + + /** + * Stores already opened buckets for reuse. + */ + private final Map bucketCache = new ConcurrentHashMap<>(); + + /** + * Connect to a Couchbase cluster with a username and a password as credentials. + *

    + * This is the simplest (and recommended) method to connect to the cluster if you do not need to provide any + * custom options. + *

    + * The first argument (the connection string in its simplest form) is used to supply the hostnames of the cluster. In + * development it is OK to only pass in one hostname (or IP address), but in production we recommend passing in at + * least 3 nodes of the cluster (comma separated). The reason is that if one or more of the nodes are not reachable + * the client will still be able to bootstrap (and your application will become more resilient as a result). + *

    + * Here is how you specify one node to use for bootstrapping: + *

    +   * Cluster cluster = Cluster.connect("127.0.0.1", "user", "password"); // ok during development
    +   * 
    + * This is what we recommend in production: + *
    +   * Cluster cluster = Cluster.connect("host1,host2,host3", "user", "password"); // recommended in production
    +   * 
    + * It is important to understand that the SDK will only use the bootstrap ("seed nodes") host list to establish an + * initial contact with the cluster. Once the configuration is loaded this list is discarded and the client will + * connect to all nodes based on this configuration. + *

    + * This method will return immediately and the SDK will try to establish all the necessary resources and connections + * in the background. This means that depending on how fast it can be bootstrapped, the first couple cluster-level + * operations like {@link #query(String)} will take a bit longer. If you want to wait explicitly until those resources + * are available, you can use the {@link #waitUntilReady(Duration)} method before running any of them: + *

    +   * Cluster cluster = Cluster.connect("host1,host2,host3", "user", "password");
    +   * cluster.waitUntilReady(Duration.ofSeconds(5));
    +   * QueryResult result = cluster.query("select * from bucket limit 1");
    +   * 
    + * + * @param connectionString connection string used to locate the Couchbase cluster. + * @param username the name of the user with appropriate permissions on the cluster. + * @param password the password of the user with appropriate permissions on the cluster. + * @return the instantiated {@link Cluster}. + */ + public static Cluster connect(final String connectionString, final String username, final String password) { + return connect(connectionString, clusterOptions(PasswordAuthenticator.create(username, password))); + } + + /** + * Connect to a Couchbase cluster with custom options. + *

    + * You likely want to use this over the simpler {@link #connect(String, String, String)} if: + *

      + *
    • A custom {@link ClusterEnvironment}
    • + *
    • Or a custom {@link Authenticator}
    • + *
    + * needs to be provided. + *

    + * A custom environment can be passed in like this: + *

    +   * // on bootstrap:
    +   * ClusterEnvironment environment = ClusterEnvironment.builder().build();
    +   * Cluster cluster = Cluster.connect(
    +   *   "127.0.0.1",
    +   *   clusterOptions("user", "password").environment(environment)
    +   * );
    +   *
    +   * // on shutdown:
    +   * cluster.disconnect();
    +   * environment.shutdown();
    +   * 
    + * It is VERY important to shut down the environment when being passed in separately (as shown in + * the code sample above) and AFTER the cluster is disconnected. This will ensure an orderly shutdown + * and makes sure that no resources are left lingering. + *

    + * If you want to pass in a custom {@link Authenticator}, it is likely because you are setting up certificate-based + * authentication instead of using a username and a password directly. Remember to also enable TLS. + *

    +   * ClusterEnvironment environment = ClusterEnvironment
    +   *   .builder()
    +   *   .securityConfig(SecurityConfig.enableTls(true))
    +   *   .build();
    +   *
    +   * Authenticator authenticator = CertificateAuthenticator.fromKey(...);
    +   *
    +   * Cluster cluster = Cluster.connect(
    +   *   "127.0.0.1",
    +   *   clusterOptions(authenticator).environment(environment)
    +   * );
    +   * 
    + * This method will return immediately and the SDK will try to establish all the necessary resources and connections + * in the background. This means that depending on how fast it can be bootstrapped, the first couple cluster-level + * operations like {@link #query(String)} will take a bit longer. If you want to wait explicitly until those resources + * are available, you can use the {@link #waitUntilReady(Duration)} method before running any of them: + *
    +   * Cluster cluster = Cluster.connect("host1,host2,host3", "user", "password");
    +   * cluster.waitUntilReady(Duration.ofSeconds(5));
    +   * QueryResult result = cluster.query("select * from bucket limit 1");
    +   * 
    + * + * @param connectionString connection string used to locate the Couchbase cluster. + * @param options custom options when creating the cluster. + * @return the instantiated {@link Cluster}. + */ + public static Cluster connect(final String connectionString, final ClusterOptions options) { + notNullOrEmpty(connectionString, "ConnectionString"); + notNull(options, "ClusterOptions"); + + final ClusterOptions.Built opts = options.build(); + final Supplier environmentSupplier = extractClusterEnvironment(connectionString, opts); + return new Cluster( + environmentSupplier, + opts.authenticator(), + seedNodesFromConnectionString(connectionString, environmentSupplier.get()) + ); + } + + /** + * Connect to a Couchbase cluster with a list of seed nodes and custom options. + *

    + * Note that you likely only want to use this method if you need to pass in custom ports for specific seed nodes + * during bootstrap. Otherwise we recommend relying on the simpler {@link #connect(String, String, String)} method + * instead. + *

    + * The following example shows how to bootstrap against a node with custom KV and management ports: + *

    +   * Set seedNodes = new HashSet<>(Collections.singletonList(
    +   *   SeedNode.create("127.0.0.1", Optional.of(12000), Optional.of(9000))
    +   * ));
    +   * Cluster cluster Cluster.connect(seedNodes, clusterOptions("user", "password"));
    +   * 
    + * @param seedNodes the seed nodes used to connect to the cluster. + * @param options custom options when creating the cluster. + * @return the instantiated {@link Cluster}. + */ + public static Cluster connect(final Set seedNodes, final ClusterOptions options) { + notNullOrEmpty(seedNodes, "SeedNodes"); + notNull(options, "ClusterOptions"); + + final ClusterOptions.Built opts = options.build(); + return new Cluster(extractClusterEnvironment(null, opts), opts.authenticator(), seedNodes); + } + + /** + * Creates a new cluster from a {@link ClusterEnvironment}. + * + * @param environment the environment to use. + * @param authenticator the authenticator to use. + * @param seedNodes the seed nodes to bootstrap from. + */ + private Cluster(final Supplier environment, final Authenticator authenticator, + final Set seedNodes) { + this.asyncCluster = new AsyncCluster(environment, authenticator, seedNodes); + this.reactiveCluster = new ReactiveCluster(asyncCluster); + this.searchIndexManager = new SearchIndexManager(asyncCluster.searchIndexes()); + this.userManager = new UserManager(asyncCluster.users()); + this.bucketManager = new BucketManager(asyncCluster.buckets()); + this.queryIndexManager = new QueryIndexManager(asyncCluster.queryIndexes()); + this.analyticsIndexManager = new AnalyticsIndexManager(this); + this.eventingFunctionManager = new EventingFunctionManager(asyncCluster.eventingFunctions()); + } + + /** + * Provides access to the related {@link AsyncCluster}. + *

    + * Note that the {@link AsyncCluster} is considered advanced API and should only be used to get the last drop + * of performance or if you are building higher-level abstractions on top. If in doubt, we recommend using the + * {@link #reactive()} API instead. + */ + public AsyncCluster async() { + return asyncCluster; + } + + /** + * Provides access to the related {@link ReactiveCluster}. + */ + public ReactiveCluster reactive() { + return reactiveCluster; + } + + /** + * Provides access to the underlying {@link Core}. + * + *

    This is advanced and volatile API - it might change any time without notice. Use with care!

    + */ + @Stability.Volatile + public Core core() { + return asyncCluster.core(); + } + + /** + * The user manager allows to manage users and groups. + */ + public UserManager users() { + return userManager; + } + + /** + * The bucket manager allows to perform administrative tasks on buckets and their resources. + */ + public BucketManager buckets() { + return bucketManager; + } + + /** + * The analytics index manager allows to modify and create indexes for the analytics service. + */ + public AnalyticsIndexManager analyticsIndexes() { + return analyticsIndexManager; + } + + /** + * The query index manager allows to modify and create indexes for the query service. + */ + public QueryIndexManager queryIndexes() { + return queryIndexManager; + } + + /** + * The search index manager allows to modify and create indexes for the search service. + */ + public SearchIndexManager searchIndexes() { + return searchIndexManager; + } + + /** + * Provides access to the eventing function management services. + */ + @Stability.Uncommitted + public EventingFunctionManager eventingFunctions() { + return eventingFunctionManager; + } + + /** + * Provides access to the used {@link ClusterEnvironment}. + */ + public ClusterEnvironment environment() { + return asyncCluster.environment(); + } + + /** + * Performs a query against the query (N1QL) services. + * + * @param statement the N1QL query statement. + * @return the {@link QueryResult} once the response arrives successfully. + * @throws TimeoutException if the operation times out before getting a result. + * @throws CouchbaseException for all other error reasons (acts as a base type and catch-all). + */ + public QueryResult query(final String statement) { + return query(statement, DEFAULT_QUERY_OPTIONS); + } + + /** + * Performs a query against the query (N1QL) services with custom options. + * + * @param statement the N1QL query statement as a raw string. + * @param options the custom options for this query. + * @return the {@link QueryResult} once the response arrives successfully. + * @throws TimeoutException if the operation times out before getting a result. + * @throws CouchbaseException for all other error reasons (acts as a base type and catch-all). + */ + public QueryResult query(final String statement, final QueryOptions options) { + return block(async().query(statement, options)); + } + + /** + * Performs an analytics query with default {@link AnalyticsOptions}. + * + * @param statement the query statement as a raw string. + * @return the {@link AnalyticsResult} once the response arrives successfully. + * @throws TimeoutException if the operation times out before getting a result. + * @throws CouchbaseException for all other error reasons (acts as a base type and catch-all). + */ + public AnalyticsResult analyticsQuery(final String statement) { + return analyticsQuery(statement, DEFAULT_ANALYTICS_OPTIONS); + } + + /** + * Performs an analytics query with custom {@link AnalyticsOptions}. + * + * @param statement the query statement as a raw string. + * @param options the custom options for this query. + * @return the {@link AnalyticsResult} once the response arrives successfully. + * @throws TimeoutException if the operation times out before getting a result. + * @throws CouchbaseException for all other error reasons (acts as a base type and catch-all). + */ + public AnalyticsResult analyticsQuery(final String statement, final AnalyticsOptions options) { + return block(async().analyticsQuery(statement, options)); + } + + /** + * Performs a Full Text Search (FTS) query with default {@link SearchOptions}. + * + * @param query the query, in the form of a {@link SearchQuery} + * @return the {@link SearchRequest} once the response arrives successfully. + * @throws TimeoutException if the operation times out before getting a result. + * @throws CouchbaseException for all other error reasons (acts as a base type and catch-all). + */ + public SearchResult searchQuery(final String indexName, final SearchQuery query) { + return searchQuery(indexName, query, DEFAULT_SEARCH_OPTIONS); + } + + /** + * Performs a Full Text Search (FTS) query with custom {@link SearchOptions}. + * + * @param query the query, in the form of a {@link SearchQuery} + * @param options the custom options for this query. + * @return the {@link SearchRequest} once the response arrives successfully. + * @throws TimeoutException if the operation times out before getting a result. + * @throws CouchbaseException for all other error reasons (acts as a base type and catch-all). + */ + public SearchResult searchQuery(final String indexName, final SearchQuery query, final SearchOptions options) { + return block(asyncCluster.searchQuery(indexName, query, options)); + } + + /** + * Opens a {@link Bucket} with the given name. + * + * @param bucketName the name of the bucket to open. + * @return a {@link Bucket} once opened. + */ + public Bucket bucket(final String bucketName) { + return bucketCache.computeIfAbsent(bucketName, n -> new Bucket(asyncCluster.bucket(n))); + } + + /** + * Performs a non-reversible disconnect of this {@link Cluster}. + *

    + * If this method is used, the default disconnect timeout on the environment is used. Please use the companion + * overload ({@link #disconnect(Duration)} if you want to provide a custom duration. + *

    + * If a custom {@link ClusterEnvironment} has been passed in during connect, it is VERY important to + * shut it down after calling this method. This will prevent any in-flight tasks to be stopped prematurely. + */ + public void disconnect() { + block(asyncCluster.disconnect()); + } + + /** + * Performs a non-reversible disconnect of this {@link Cluster}. + *

    + * If a custom {@link ClusterEnvironment} has been passed in during connect, it is VERY important to + * shut it down after calling this method. This will prevent any in-flight tasks to be stopped prematurely. + * + * @param timeout allows to override the default disconnect duration. + */ + public void disconnect(final Duration timeout) { + block(asyncCluster.disconnect(timeout)); + } + + /** + * Runs a diagnostic report on the current state of the cluster from the SDKs point of view. + *

    + * Please note that it does not perform any I/O to do this, it will only use the current known state of the cluster + * to assemble the report (so, if for example no N1QL query has been run the socket pool might be empty and as + * result not show up in the report). + * + * @return the {@link DiagnosticsResult} once complete. + */ + public DiagnosticsResult diagnostics() { + return block(asyncCluster.diagnostics(DEFAULT_DIAGNOSTICS_OPTIONS)); + } + + /** + * Runs a diagnostic report with custom options on the current state of the cluster from the SDKs point of view. + *

    + * Please note that it does not perform any I/O to do this, it will only use the current known state of the cluster + * to assemble the report (so, if for example no N1QL query has been run the socket pool might be empty and as + * result not show up in the report). + * + * @param options options that allow to customize the report. + * @return the {@link DiagnosticsResult} once complete. + */ + public DiagnosticsResult diagnostics(final DiagnosticsOptions options) { + return block(asyncCluster.diagnostics(options)); + } + + /** + * Performs application-level ping requests against services in the couchbase cluster. + *

    + * Note that this operation performs active I/O against services and endpoints to assess their health. If you do + * not wish to perform I/O, consider using the {@link #diagnostics()} instead. You can also combine the functionality + * of both APIs as needed, which is {@link #waitUntilReady(Duration)} is doing in its implementation as well. + * + * @return the {@link PingResult} once complete. + */ + public PingResult ping() { + return block(asyncCluster.ping()); + } + + /** + * Performs application-level ping requests with custom options against services in the couchbase cluster. + *

    + * Note that this operation performs active I/O against services and endpoints to assess their health. If you do + * not wish to perform I/O, consider using the {@link #diagnostics(DiagnosticsOptions)} instead. You can also combine + * the functionality of both APIs as needed, which is {@link #waitUntilReady(Duration)} is doing in its + * implementation as well. + * + * @return the {@link PingResult} once complete. + */ + public PingResult ping(final PingOptions options) { + return block(asyncCluster.ping(options)); + } + + /** + * Waits until the desired {@link ClusterState} is reached. + *

    + * This method will wait until either the cluster state is "online", or the timeout is reached. Since the SDK is + * bootstrapping lazily, this method allows to eagerly check during bootstrap if all of the services are online + * and usable before moving on. + * + * @param timeout the maximum time to wait until readiness. + */ + public void waitUntilReady(final Duration timeout) { + block(asyncCluster.waitUntilReady(timeout)); + } + + /** + * Waits until the desired {@link ClusterState} is reached. + *

    + * This method will wait until either the cluster state is "online" by default, or the timeout is reached. Since the + * SDK is bootstrapping lazily, this method allows to eagerly check during bootstrap if all of the services are online + * and usable before moving on. You can tune the properties through {@link WaitUntilReadyOptions}. + * + * @param timeout the maximum time to wait until readiness. + * @param options the options to customize the readiness waiting. + */ + public void waitUntilReady(final Duration timeout, final WaitUntilReadyOptions options) { + block(asyncCluster.waitUntilReady(timeout, options)); + } + +} + diff --git a/src/main/java/com/couchbase/client/java/ClusterInterface.java b/src/main/java/com/couchbase/client/java/ClusterInterface.java new file mode 100644 index 000000000..c6cb97e06 --- /dev/null +++ b/src/main/java/com/couchbase/client/java/ClusterInterface.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018 Couchbase, Inc. + * + * 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 + * + * http://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 com.couchbase.client.java; + +import com.couchbase.client.core.Core; +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; +import com.couchbase.client.core.diagnostics.PingResult; +import com.couchbase.client.core.env.Authenticator; +import com.couchbase.client.core.env.PasswordAuthenticator; +import com.couchbase.client.core.env.SeedNode; +import com.couchbase.client.java.analytics.AnalyticsOptions; +//import com.couchbase.client.java.analytics.AnalyticsResult; +import com.couchbase.client.java.diagnostics.DiagnosticsOptions; +import com.couchbase.client.java.diagnostics.PingOptions; +import com.couchbase.client.java.diagnostics.WaitUntilReadyOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.manager.analytics.AnalyticsIndexManager; +import com.couchbase.client.java.manager.bucket.BucketManager; +import com.couchbase.client.java.manager.eventing.EventingFunctionManager; +import com.couchbase.client.java.manager.query.QueryIndexManager; +import com.couchbase.client.java.manager.search.SearchIndexManager; +import com.couchbase.client.java.manager.user.UserManager; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.search.SearchOptions; +import com.couchbase.client.java.search.SearchQuery; +import com.couchbase.client.java.search.result.SearchResult; + +import java.time.Duration; +import java.util.Set; +import java.util.function.Supplier; + +import static com.couchbase.client.core.util.Validators.notNull; +import static com.couchbase.client.core.util.Validators.notNullOrEmpty; +import static com.couchbase.client.java.AsyncCluster.extractClusterEnvironment; +import static com.couchbase.client.java.AsyncCluster.seedNodesFromConnectionString; +import static com.couchbase.client.java.ClusterOptions.clusterOptions; + +public interface ClusterInterface { + + AsyncCluster async(); + + ReactiveCluster reactive(); + + @Stability.Volatile + Core core(); + + UserManager users(); + + BucketManager buckets(); + + AnalyticsIndexManager analyticsIndexes(); + + QueryIndexManager queryIndexes(); + + SearchIndexManager searchIndexes(); + + @Stability.Uncommitted + EventingFunctionManager eventingFunctions(); + + ClusterEnvironment environment(); + + QueryResult query(String statement); + + QueryResult query(String statement, QueryOptions options); + + //AnalyticsResult analyticsQuery(String statement); + + // AnalyticsResult analyticsQuery(String statement, AnalyticsOptions options); + + SearchResult searchQuery(String indexName, SearchQuery query); + + SearchResult searchQuery(String indexName, SearchQuery query, SearchOptions options); + + Bucket bucket(String bucketName); + + void disconnect(); + + void disconnect(Duration timeout); + + DiagnosticsResult diagnostics(); + + DiagnosticsResult diagnostics(DiagnosticsOptions options); + + PingResult ping(); + + PingResult ping(PingOptions options); + + void waitUntilReady(Duration timeout); + + void waitUntilReady(Duration timeout, WaitUntilReadyOptions options); +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java new file mode 100644 index 000000000..26231197e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java @@ -0,0 +1,384 @@ +/* + * Copyright 2002-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.couchbase.transaction.interceptor; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Properties; +import java.util.concurrent.ConcurrentMap; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.lang.Nullable; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.interceptor.DefaultTransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.interceptor.TransactionProxyFactoryBean; +import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * AOP Alliance MethodInterceptor for declarative transaction + * management using the common Spring transaction infrastructure + * ({@link org.springframework.transaction.PlatformTransactionManager}/ + * {@link org.springframework.transaction.ReactiveTransactionManager}). + * + *

    Derives from the {@link TransactionAspectSupport} class which + * contains the integration with Spring's underlying transaction API. + * TransactionInterceptor simply calls the relevant superclass methods + * such as {@link #invokeWithinTransaction} in the correct order. + * + *

    TransactionInterceptors are thread-safe. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @see TransactionProxyFactoryBean + * @see org.springframework.aop.framework.ProxyFactoryBean + * @see org.springframework.aop.framework.ProxyFactory + */ +@SuppressWarnings("serial") +public class CouchbaseTransactionInterceptor extends TransactionInterceptor implements MethodInterceptor, Serializable { + +// NOTE: This class must not implement Serializable because it serves as base + // class for AspectJ aspects (which are not allowed to implement Serializable)! + + /** + * Vavr library present on the classpath? + */ + private static final boolean vavrPresent = ClassUtils.isPresent( + "io.vavr.control.Try", TransactionAspectSupport.class.getClassLoader()); + + /** + * Reactive Streams API present on the classpath? + */ + private static final boolean reactiveStreamsPresent = + ClassUtils.isPresent("org.reactivestreams.Publisher", TransactionAspectSupport.class.getClassLoader()); + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private final ReactiveAdapterRegistry reactiveAdapterRegistry; + + private final ConcurrentMap transactionManagerCache = + new ConcurrentReferenceHashMap<>(4); + + /** + * Create a new TransactionInterceptor. + *

    Transaction manager and transaction attributes still need to be set. + * @see #setTransactionManager + * @see #setTransactionAttributes(java.util.Properties) + * @see #setTransactionAttributeSource(TransactionAttributeSource) + */ + public CouchbaseTransactionInterceptor() { + if (reactiveStreamsPresent) { + this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); + } + else { + this.reactiveAdapterRegistry = null; + } + } + + /** + * Create a new TransactionInterceptor. + * @param ptm the default transaction manager to perform the actual transaction management + * @param tas the attribute source to be used to find transaction attributes + * @since 5.2.5 + * @see #setTransactionManager + * @see #setTransactionAttributeSource + */ + public CouchbaseTransactionInterceptor(TransactionManager ptm, TransactionAttributeSource tas) { + this(); + setTransactionManager(ptm); + setTransactionAttributeSource(tas); + } + + /** + * Create a new TransactionInterceptor. + * @param ptm the default transaction manager to perform the actual transaction management + * @param tas the attribute source to be used to find transaction attributes + * @see #setTransactionManager + * @see #setTransactionAttributeSource + * @deprecated as of 5.2.5, in favor of + * {@link #CouchbaseTransactionInterceptor(TransactionManager, TransactionAttributeSource)} + */ + @Deprecated + public CouchbaseTransactionInterceptor(PlatformTransactionManager ptm, TransactionAttributeSource tas) { + this(); + setTransactionManager(ptm); + setTransactionAttributeSource(tas); + } + + /** + * Create a new TransactionInterceptor. + * @param ptm the default transaction manager to perform the actual transaction management + * @param attributes the transaction attributes in properties format + * @see #setTransactionManager + * @see #setTransactionAttributes(java.util.Properties) + * @deprecated as of 5.2.5, in favor of {@link #setTransactionAttributes(Properties)} + */ + @Deprecated + public CouchbaseTransactionInterceptor(PlatformTransactionManager ptm, Properties attributes) { + this(); + setTransactionManager(ptm); + setTransactionAttributes(attributes); + } + + + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + // Work out the target class: may be {@code null}. + // The TransactionAttributeSource should be passed the target class + // as well as the method, which may be from an interface. + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + + // Adapt to TransactionAspectSupport's invokeWithinTransaction... + return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() { + @Override + @Nullable + public Object proceedWithInvocation() throws Throwable { + return invocation.proceed(); + } + @Override + public Object getTarget() { + return invocation.getThis(); + } + @Override + public Object[] getArguments() { + return invocation.getArguments(); + } + }); + } + + /** + * General delegate for around-advice-based subclasses, delegating to several other template + * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager} + * as well as regular {@link PlatformTransactionManager} implementations and + * {@link ReactiveTransactionManager} implementations for reactive return types. + * @param method the Method being invoked + * @param targetClass the target class that we're invoking the method on + * @param invocation the callback to use for proceeding with the target invocation + * @return the return value of the method, if any + * @throws Throwable propagated from the target invocation + */ + @Nullable + protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + + // If the transaction attribute is null, the method is non-transactional. + TransactionAttributeSource tas = getTransactionAttributeSource(); + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + final TransactionManager tm = determineTransactionManager(txAttr); + + if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) { + return super.invokeWithinTransaction(method, targetClass, invocation); + } + + PlatformTransactionManager ptm = asPlatformTransactionManager(tm); + + if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { + return super.invokeWithinTransaction(method, targetClass, invocation); + } + + else { + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + Object result; + final ThrowableHolder throwableHolder = new ThrowableHolder(); + + // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in. + try { + result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> { + TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status); + try { + Object retVal = invocation.proceedWithInvocation(); + if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { + // Set rollback-only in case of Vavr failure matching our rollback rules... + retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); + } + return retVal; + } + catch (Throwable ex) { + if (txAttr.rollbackOn(ex)) { + // A RuntimeException: will lead to a rollback. + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + else { + throw new ThrowableHolderException(ex); + } + } + else { + // A normal return value: will lead to a commit. + throwableHolder.throwable = ex; + return null; + } + } + finally { + cleanupTransactionInfo(txInfo); + } + }); + } + catch (ThrowableHolderException ex) { + throw ex.getCause(); + } + catch (TransactionSystemException ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + ex2.initApplicationException(throwableHolder.throwable); + } + throw ex2; + } + catch (Throwable ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + } + throw ex2; + } + + // Check result state: It might indicate a Throwable to rethrow. + if (throwableHolder.throwable != null) { + throw throwableHolder.throwable; + } + return result; + } + } + + // from TransactionAspectSupport + @Nullable + private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) { + if (transactionManager == null || transactionManager instanceof PlatformTransactionManager) { + return (PlatformTransactionManager) transactionManager; + } + else { + throw new IllegalStateException( + "Specified transaction manager is not a PlatformTransactionManager: " + transactionManager); + } + } + + // from TransactionAspectSupport + private String methodIdentification(Method method, @Nullable Class targetClass, + @Nullable TransactionAttribute txAttr) { + + String methodIdentification = methodIdentification(method, targetClass); + if (methodIdentification == null) { + if (txAttr instanceof DefaultTransactionAttribute) { + methodIdentification = ((DefaultTransactionAttribute) txAttr).getDescriptor(); + } + if (methodIdentification == null) { + methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); + } + } + return methodIdentification; + } + + // from TransactionAspectSupport + /** + * Internal holder class for a Throwable, used as a RuntimeException to be + * thrown from a TransactionCallback (and subsequently unwrapped again). + */ + @SuppressWarnings("serial") + private static class ThrowableHolderException extends RuntimeException { + + public ThrowableHolderException(Throwable throwable) { + super(throwable); + } + + @Override + public String toString() { + return getCause().toString(); + } + } + + // from TransactionAspectSupport + /** + * Internal holder class for a Throwable in a callback transaction model. + */ + private static class ThrowableHolder { + + @Nullable + public Throwable throwable; + } + + // From TransactionAspectSupport + /** + * Inner class to avoid a hard dependency on the Vavr library at runtime. + */ + private static class VavrDelegate { + + public static boolean isVavrTry(Object retVal) { + return false; // (retVal instanceof Try); + } + + public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAttr, TransactionStatus status) { + throw new RuntimeException("no Vavr support"); + /* + return ((Try) retVal).onFailure(ex -> { + if (txAttr.rollbackOn(ex)) { + status.setRollbackOnly(); + } + }); + */ + } + } + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void writeObject(ObjectOutputStream oos) throws IOException { + // Rely on default serialization, although this class itself doesn't carry state anyway... + oos.defaultWriteObject(); + + // Deserialize superclass fields. + oos.writeObject(getTransactionManagerBeanName()); + oos.writeObject(getTransactionManager()); + oos.writeObject(getTransactionAttributeSource()); + oos.writeObject(getBeanFactory()); + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, although this class itself doesn't carry state anyway... + ois.defaultReadObject(); + + // Serialize all relevant superclass fields. + // Superclass can't implement Serializable because it also serves as base class + // for AspectJ aspects (which are not allowed to implement Serializable)! + setTransactionManagerBeanName((String) ois.readObject()); + setTransactionManager((PlatformTransactionManager) ois.readObject()); + setTransactionAttributeSource((TransactionAttributeSource) ois.readObject()); + setBeanFactory((BeanFactory) ois.readObject()); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index ee92d1588..7ee1c3862 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -23,8 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import lombok.Data; -import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -40,8 +38,11 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; import org.springframework.context.support.GenericApplicationContext; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.DuplicateKeyException; @@ -61,11 +62,13 @@ import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.TransactionsWrapper; +import org.springframework.data.couchbase.transaction.interceptor.CouchbaseTransactionInterceptor; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -75,8 +78,13 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.ReactiveTransaction; import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.transaction.reactive.TransactionContextManager; import org.springframework.transaction.reactive.TransactionSynchronizationManager; import org.springframework.transaction.reactive.TransactionalOperator; @@ -125,7 +133,7 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT public static void beforeAll() { callSuperBeforeAll(new Object() {}); context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionIntegrationTests.Config.class, - PersonService.class); + PersonService.class, CouchbasePersonTransactionIntegrationTests.TransactionInterception.class); } @AfterAll @@ -147,11 +155,12 @@ public void beforeEachTest() { try { couchbaseClientFactory.getBucket().defaultCollection().remove(walterWhite.getId().toString()); } catch (Exception ex) { - //System.err.println(ex); + // System.err.println(ex); } } - /* + /* Not used in this class. The class itself is not @Transaction + List>> assertionList; @BeforeTransaction @@ -312,7 +321,7 @@ public void insertPersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); try { rxCBTmpl.removeById(Person.class).one(person.getId().toString()); - } catch(DocumentNotFoundException dnfe){} + } catch (DocumentNotFoundException dnfe) {} Mono result = transactions.reactive(ctx -> { // get the ctx ClientSession clientSession = couchbaseClientFactory @@ -412,36 +421,38 @@ public void replaceWithCasConflictResolvedViaRetry() { public void wrapperReplaceWithCasConflictResolvedViaRetry() { Person person = new Person(1, "Walter", "White"); Person switchedPerson = new Person(1, "Dave", "Reynolds"); - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); - t.start(); - cbTmpl.insertById(Person.class).one(person); - AtomicInteger tryCount = new AtomicInteger(0); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(transactions, - reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.reactive(ctx -> { - tryCount.incrementAndGet(); - return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // - .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)).then(); - }); - TransactionResult txResult = result.block(); - stop.set(true); - System.out.println("txResult: "+txResult); + + for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + cbTmpl.insertById(Person.class).one(person); + tryCount.set(0); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(transactions, reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.reactive(ctx -> { + System.err.println("try: " + tryCount.incrementAndGet()); + return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)).then(); + }); + TransactionResult txResult = result.block(); + stop.set(true); + System.out.println("txResult: " + txResult); + if (tryCount.get() > 1) { + break; + } + } Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); } /** - * This does process retries - by - * CallbackTransactionManager.execute() -> transactions.run() -> executeTransaction() -> retryWhen. - * - * The CallbackTransactionManager only finds the resources in the Thread - it doesn't find it in the context. - * - * It might be nice to use the context for both - but I'm not sure if that is possible - mostly due - * to ExecutableFindById.one() calling reactive.one().block() instead of returning a publisher which could - * have .contextWrite() called on it. + * This does process retries - by CallbackTransactionManager.execute() -> transactions.run() -> executeTransaction() + * -> retryWhen. The CallbackTransactionManager only finds the resources in the Thread - it doesn't find it in the + * context. It might be nice to use the context for both - but I'm not sure if that is possible - mostly due to + * ExecutableFindById.one() calling reactive.one().block() instead of returning a publisher which could have + * .contextWrite() called on it. */ @Test public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { @@ -454,17 +465,16 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { AtomicInteger tryCount = new AtomicInteger(0); Person p = personService.declarativeFindReplacePersonCallback(person, tryCount); stop.set(true); - System.out.println("person: "+p); + System.out.println("person: " + p); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); } /** - * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. - * I don't know why it isn't retried. This seems like it is due to the functioning of - * AbstractReactiveTransactionManager - */ + * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. I don't know why + * it isn't retried. This seems like it is due to the functioning of AbstractReactiveTransactionManager + */ @Test public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { Person person = new Person(1, "Walter", "White"); @@ -476,7 +486,7 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { AtomicInteger tryCount = new AtomicInteger(0); Person p = personService.declarativeFindReplacePersonReactive(person, tryCount).block(); stop.set(true); - System.out.println("person: "+p); + System.out.println("person: " + p); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); @@ -484,9 +494,8 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { @Test /** - * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. - * I don't know why it isn't retried. This seems like it is due to the functioning of - * AbstractPlatformTransactionManager + * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. I don't know why + * it isn't retried. This seems like it is due to the functioning of AbstractPlatformTransactionManager */ public void replaceWithCasConflictResolvedViaRetryAnnotated() { Person person = new Person(1, "Walter", "White"); @@ -498,7 +507,7 @@ public void replaceWithCasConflictResolvedViaRetryAnnotated() { AtomicInteger tryCount = new AtomicInteger(0); Person p = personService.declarativeFindReplacePerson(person, tryCount); stop.set(true); - System.out.println("person: "+p); + System.out.println("person: " + p); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); @@ -792,7 +801,7 @@ void assertThrowsCause(Class exceptionClass, Class causeClass, FunctiongetNumber("order")); + // } + return advisor; + } + + } + @Service @Component @EnableTransactionManagement @@ -981,7 +1025,7 @@ public Person declarativeSavePerson(Person person) { public Person savePersonBlocking(Person person) { if (1 == 1) - throw new RuntimeException("no implemented"); + throw new RuntimeException("not implemented"); assertInAnnotationTransaction(true); return personOperations.insertById(Person.class).one(person); @@ -995,8 +1039,8 @@ public Person declarativeSavePersonErrors(Person person) { return p; } - @Autowired - CouchbaseCallbackTransactionManager callbackTm; + @Autowired CouchbaseCallbackTransactionManager callbackTm; + /** * to execute while ThreadReplaceloop() is running should force a retry * @@ -1006,9 +1050,12 @@ public Person declarativeSavePersonErrors(Person person) { @Transactional(transactionManager = BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePersonCallback try: "+tryCount.incrementAndGet()); - System.err.println("declarativeFindReplacePersonCallback cluster : "+callbackTm.template().getCouchbaseClientFactory().getCluster().block()); - System.err.println("declarativeFindReplacePersonCallback resourceHolder : "+org.springframework.transaction.support.TransactionSynchronizationManager.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); + System.err.println("declarativeFindReplacePersonCallback cluster : " + + callbackTm.template().getCouchbaseClientFactory().getCluster().block()); + System.err.println("declarativeFindReplacePersonCallback resourceHolder : " + + org.springframework.transaction.support.TransactionSynchronizationManager + .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); Person p = personOperations.findById(Person.class).one(person.getId().toString()); return personOperations.replaceById(Person.class).one(p); } @@ -1022,7 +1069,7 @@ public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePersonReactive try: "+tryCount.incrementAndGet()); + System.err.println("declarativeFindReplacePersonReactive try: " + tryCount.incrementAndGet()); /* NoTransactionInContextException TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { System.err.println("declarativeFindReplacePersonReactive reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); @@ -1030,7 +1077,7 @@ public Mono declarativeFindReplacePersonReactive(Person person, AtomicIn }).block(); */ return personOperationsRx.findById(Person.class).one(person.getId().toString()) - .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p)); + .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p)); } /** @@ -1042,12 +1089,11 @@ public Mono declarativeFindReplacePersonReactive(Person person, AtomicIn @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) // doesn't retry public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePerson try: "+tryCount.incrementAndGet()); + System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet()); Person p = personOperations.findById(Person.class).one(person.getId().toString()); return personOperations.replaceById(Person.class).one(p); } - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // doesn't retry public Mono declarativeSavePersonReactive(Person person) { assertInAnnotationTransaction(true); diff --git a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java index ffeeb6b5d..bbb4c3fa3 100644 --- a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java @@ -77,7 +77,7 @@ public static void beforeAll() { List fieldList = new ArrayList<>(); fieldList.add("parentId"); - cluster.query("CREATE INDEX `parent_idx` ON default:" + bucketName() + "." + scopeName + "." + collectionName2 + cluster.query("CREATE INDEX `parent_idx` ON default:`" + bucketName() + "`." + scopeName + "." + collectionName2 + "(parentId)"); } catch (IndexExistsException ife) { LOGGER.warn("IndexFailureException occurred - ignoring: ", ife.toString()); From b6989d834af45133b511157188cb5aa7ea3079a8 Mon Sep 17 00:00:00 2001 From: Graham Pople Date: Tue, 3 May 2022 19:22:59 +0100 Subject: [PATCH 5/9] Datacouch 1145 transaction support (#1423) * Porting to SDK-integrated version of transactions The transactions logic exists in the Java SDK as of 3.3.0, with a slightly different API. This is the first effort at the port, which literally just compiles. It will not run as crucial code has been commented and todo-ed. There is work remaining to figure out how to complete the port, as some crucial parts (such as ctx.commit() and ctx.rollback()) have been intentionally removed. * Continuing work to get the ExtSDKIntegration port working Trying to transition to CallbackPreferring manager. * Added CouchbaseSimpleCallbackTransactionManager, the simplest possible implementation of CallbackPreferringTransactionManager, combined with a simpler approach to ThreadLocal storage in ReactiveInsertByIdSupport. Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now passing. * Adding WIP get-and-replace @Transactional support (Not yet working as CAS/version field in Person is not populated correctly.) --- pom.xml | 27 +- .../com/couchbase/client/java/Cluster.java | 11 + .../AttemptContextReactiveAccessor.java | 101 +- .../transactions/TransactionsReactive.java | 1506 ++++++++--------- .../demo/CouchbaseTransactionManager.java | 92 +- .../demo/CouchbaseTransactionalTemplate.java | 26 +- .../demo/SpringTransactionGetResult.java | 2 +- .../couchbase/CouchbaseClientFactory.java | 7 +- .../ReactiveCouchbaseClientFactory.java | 11 +- .../SimpleCouchbaseClientFactory.java | 35 +- .../SimpleReactiveCouchbaseClientFactory.java | 38 +- .../AbstractCouchbaseConfiguration.java | 53 +- .../data/couchbase/config/BeanNames.java | 2 +- .../data/couchbase/core/GenericSupport.java | 61 + .../ReactiveFindByIdOperationSupport.java | 65 +- .../ReactiveFindByQueryOperationSupport.java | 16 +- .../ReactiveInsertByIdOperationSupport.java | 92 +- .../ReactiveRemoveByIdOperationSupport.java | 16 +- ...ReactiveRemoveByQueryOperationSupport.java | 6 +- .../ReactiveReplaceByIdOperationSupport.java | 122 +- .../ReactiveUpsertByIdOperationSupport.java | 2 +- .../core/mapping/CouchbaseDocument.java | 3 + .../couchbase/core/query/OptionsBuilder.java | 23 +- .../transaction/SDCouchbaseTransactions.java | 76 - .../repository/TransactionResult.java | 1 + .../support/TransactionResultHolder.java | 22 +- .../couchbase/transaction/ClientSession.java | 23 +- .../transaction/ClientSessionImpl.java | 156 +- .../transaction/ClientSessionOptions.java | 3 +- .../CouchbaseAttemptContextReactive.java | 10 +- .../CouchbaseCallbackTransactionManager.java | 589 +++---- .../transaction/CouchbaseResourceHolder.java | 2 +- ...hbaseSimpleCallbackTransactionManager.java | 92 + .../CouchbaseSimpleTransactionManager.java | 49 + .../transaction/CouchbaseStuffHandle.java | 29 +- .../CouchbaseTransactionDefinition.java | 15 +- .../CouchbaseTransactionManager.java | 28 +- ...hbaseTransactionalOperatorNonReactive.save | 8 +- .../ReactiveCouchbaseResourceHolder.java | 1 + .../ReactiveCouchbaseTransactionManager.java | 42 +- .../transaction/TransactionOptions.java | 19 - .../transaction/TransactionsWrapper.java | 160 +- .../internal/AsyncClientSession.java | 2 +- ...basePersonTransactionIntegrationTests.java | 192 +-- ...onTransactionReactiveIntegrationTests.java | 1226 +++++++------- ...uchbaseReactiveTransactionNativeTests.java | 46 +- ...eTemplateTransaction2IntegrationTests.java | 14 +- ...seTemplateTransactionIntegrationTests.java | 10 - .../CouchbaseTransactionNativeTests.save | 34 +- .../util/ClusterAwareIntegrationTests.java | 7 +- 50 files changed, 2587 insertions(+), 2586 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/GenericSupport.java delete mode 100644 src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleTransactionManager.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java diff --git a/pom.xml b/pom.xml index 99a817e2b..eb583ef70 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,6 @@ 3.2.5 3.2.5 2.7.0-SNAPSHOT - 1.2.2 spring.data.couchbase - - com.couchbase.client - couchbase-transactions - ${couchbase-transactions} - - - com.couchbase.client - java-client - - - - org.springframework spring-context-support @@ -244,18 +231,6 @@ 4.0.3 test - - com.couchbase.client - couchbase-transactions - ${couchbase-transactions} - compile - - - com.couchbase.client - java-client - - - org.testcontainers testcontainers @@ -264,7 +239,7 @@ com.couchbase.client java-client - 3.2.5 + 3.3.0-SNAPSHOT diff --git a/src/main/java/com/couchbase/client/java/Cluster.java b/src/main/java/com/couchbase/client/java/Cluster.java index 907b58241..0d3b3601c 100644 --- a/src/main/java/com/couchbase/client/java/Cluster.java +++ b/src/main/java/com/couchbase/client/java/Cluster.java @@ -44,6 +44,7 @@ import com.couchbase.client.java.search.SearchOptions; import com.couchbase.client.java.search.SearchQuery; import com.couchbase.client.java.search.result.SearchResult; +import com.couchbase.client.java.transactions.Transactions; import java.time.Duration; import java.util.Map; @@ -102,6 +103,7 @@ * The SDK will only work against Couchbase Server 5.0 and later, because RBAC (role-based access control) is a first * class concept since 3.0 and therefore required. */ +// todo gp is this required? public class Cluster implements ClusterInterface { /** @@ -574,5 +576,14 @@ public void waitUntilReady(final Duration timeout, final WaitUntilReadyOptions o block(asyncCluster.waitUntilReady(timeout, options)); } + /** + * Allows access to transactions. + * + * @return the {@link Transactions} interface. + */ + @Stability.Uncommitted + public Transactions transactions() { + return new Transactions(core(), environment().jsonSerializer()); + } } diff --git a/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java index f59d110ed..5f2a0559f 100644 --- a/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java +++ b/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java @@ -17,12 +17,10 @@ package com.couchbase.transactions; import com.couchbase.client.core.annotation.Stability; -import com.couchbase.transactions.config.MergedTransactionConfig; -import com.couchbase.transactions.config.PerTransactionConfig; -import com.couchbase.transactions.config.PerTransactionConfigBuilder; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.forwards.Supported; -import com.couchbase.transactions.log.TransactionLogger; +import com.couchbase.client.core.transaction.log.CoreTransactionLogger; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import org.springframework.transaction.reactive.TransactionContext; import java.time.Duration; import java.time.temporal.ChronoUnit; @@ -30,61 +28,70 @@ import java.util.UUID; /** - * To access the AttemptContextReactive held by AttemptContext + * To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext * * @author Michael Reiche */ public class AttemptContextReactiveAccessor { - public static AttemptContextReactive getACR(AttemptContext attemptContext) { - return attemptContext.ctx(); + public static ReactiveTransactionAttemptContext getACR(TransactionAttemptContext attemptContext) { + // return attemptContext.ctx(); + // todo gp is this access needed. Could hold the raw CoreTransactionAttemptContext instead. + return null; } - public static AttemptContext from(AttemptContextReactive attemptContextReactive) { - return new AttemptContext(attemptContextReactive); + public static TransactionAttemptContext from(ReactiveTransactionAttemptContext attemptContextReactive) { + // todo gp needed? + return null; +// return new TransactionAttemptContext(attemptContextReactive); } - public static TransactionLogger getLogger(AttemptContextReactive attemptContextReactive){ - return attemptContextReactive.LOGGER; - } - @Stability.Internal - public static AttemptContextReactive newAttemptContextReactive(TransactionsReactive transactions){ - PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); - MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); - - TransactionContext overall = new TransactionContext( - transactions.cleanup().clusterData().cluster().environment().requestTracer(), - transactions.cleanup().clusterData().cluster().environment().eventBus(), - UUID.randomUUID().toString(), now(), Duration.ZERO, merged); - - String txnId = UUID.randomUUID().toString(); - overall.LOGGER.info(configDebug(transactions.config(), perConfig)); - return transactions.createAttemptContext(overall, merged, txnId); + public static CoreTransactionLogger getLogger(ReactiveTransactionAttemptContext attemptContextReactive){ + // todo gp needed? + return null; + //return attemptContextReactive; } + // todo gp needed? +// @Stability.Internal +// public static ReactiveTransactionAttemptContext newAttemptContextReactive(TransactionsReactive transactions){ +// return null; +// PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); +// MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); +// +// TransactionContext overall = new TransactionContext( +// transactions.cleanup().clusterData().cluster().environment().requestTracer(), +// transactions.cleanup().clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), now(), Duration.ZERO, merged); +// +// String txnId = UUID.randomUUID().toString(); +// overall.LOGGER.info(configDebug(transactions.config(), perConfig)); +// return transactions.createAttemptContext(overall, merged, txnId); +// } private static Duration now() { return Duration.of(System.nanoTime(), ChronoUnit.NANOS); } - static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { - StringBuilder sb = new StringBuilder(); - sb.append("library version: "); - sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); - sb.append(" config: "); - sb.append("atrs="); - sb.append(config.numAtrs()); - sb.append(", metadataCollection="); - sb.append(config.metadataCollection()); - sb.append(", expiry="); - sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); - sb.append("msecs durability="); - sb.append(config.durabilityLevel()); - sb.append(" per-txn config="); - sb.append(" durability="); - sb.append(perConfig.durabilityLevel()); - sb.append(", supported="); - sb.append(Supported.SUPPORTED); - return sb.toString(); - } + // todo gp if needed let's expose in the SDK +// static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { +// StringBuilder sb = new StringBuilder(); +// sb.append("library version: "); +// sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); +// sb.append(" config: "); +// sb.append("atrs="); +// sb.append(config.numAtrs()); +// sb.append(", metadataCollection="); +// sb.append(config.metadataCollection()); +// sb.append(", expiry="); +// sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); +// sb.append("msecs durability="); +// sb.append(config.durabilityLevel()); +// sb.append(" per-txn config="); +// sb.append(" durability="); +// sb.append(perConfig.durabilityLevel()); +// sb.append(", supported="); +// sb.append(Supported.SUPPORTED); +// return sb.toString(); +// } } diff --git a/src/main/java/com/couchbase/transactions/TransactionsReactive.java b/src/main/java/com/couchbase/transactions/TransactionsReactive.java index 1e64e803e..352135ead 100644 --- a/src/main/java/com/couchbase/transactions/TransactionsReactive.java +++ b/src/main/java/com/couchbase/transactions/TransactionsReactive.java @@ -1,753 +1,753 @@ -/* - * Copyright 2021 Couchbase, Inc. - * - * 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 - * - * http://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 com.couchbase.transactions; - -import com.couchbase.client.core.annotation.Stability; -import com.couchbase.client.core.cnc.EventBus; -import com.couchbase.client.core.retry.reactor.DefaultRetry; -import com.couchbase.client.core.retry.reactor.Jitter; -import com.couchbase.client.core.retry.reactor.RetryContext; -import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.ReactiveCollection; -import com.couchbase.client.java.ReactiveScope; -import com.couchbase.client.java.json.JsonObject; -import com.couchbase.client.java.query.ReactiveQueryResult; -import com.couchbase.transactions.cleanup.ClusterData; -import com.couchbase.transactions.cleanup.TransactionsCleanup; -import com.couchbase.transactions.components.ATR; -import com.couchbase.transactions.components.ActiveTransactionRecord; -import com.couchbase.transactions.config.MergedTransactionConfig; -import com.couchbase.transactions.config.PerTransactionConfig; -import com.couchbase.transactions.config.PerTransactionConfigBuilder; -import com.couchbase.transactions.config.SingleQueryTransactionConfig; -import com.couchbase.transactions.config.SingleQueryTransactionConfigBuilder; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.deferred.TransactionSerializedContext; -import com.couchbase.transactions.error.TransactionCommitAmbiguous; -import com.couchbase.transactions.error.TransactionExpired; -import com.couchbase.transactions.error.TransactionFailed; -import com.couchbase.transactions.error.internal.ErrorClasses; -import com.couchbase.transactions.error.external.TransactionOperationFailed; -import com.couchbase.transactions.forwards.Supported; -import com.couchbase.transactions.log.EventBusPersistedLogger; -import com.couchbase.transactions.log.PersistedLogWriter; -import com.couchbase.transactions.log.TransactionLogEvent; -import com.couchbase.transactions.support.AttemptContextFactory; -import com.couchbase.transactions.support.AttemptStates; -import com.couchbase.transactions.support.OptionsWrapperUtil; -import com.couchbase.transactions.util.DebugUtil; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; - -import static com.couchbase.transactions.error.internal.TransactionOperationFailedBuilder.createError; -import static com.couchbase.transactions.log.PersistedLogWriter.MAX_LOG_ENTRIES_DEFAULT; -import static com.couchbase.transactions.support.SpanWrapperUtil.DB_COUCHBASE_TRANSACTIONS; - -/** - * An asynchronous version of {@link Transactions}, allowing transactions to be created and run in an asynchronous - * manner. - *

    - * The main method to run transactions is {@link TransactionsReactive#run}. - */ -public class TransactionsReactive { - static final int MAX_ATTEMPTS = 1000; - private final TransactionsCleanup cleanup; - private final TransactionConfig config; - private AttemptContextFactory attemptContextFactory; - private EventBusPersistedLogger persistedLogger; - - /** - * This is package-private. Applications should create a {@link Transactions} object instead, and then call {@link - * Transactions#reactive}. - */ - static TransactionsReactive create(Cluster cluster, TransactionConfig config) { - return new TransactionsReactive(cluster, config); - } - - private TransactionsReactive(Cluster cluster, TransactionConfig config) { - Objects.requireNonNull(cluster); - Objects.requireNonNull(config); - - ClusterData clusterData = new ClusterData(cluster); - this.config = config; - this.attemptContextFactory = config.attemptContextFactory(); - MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.empty()); - cleanup = new TransactionsCleanup(merged, clusterData); - - config.persistentLoggingCollection().ifPresent(collection -> { - PersistedLogWriter persistedLogWriter = new PersistedLogWriter(collection, MAX_LOG_ENTRIES_DEFAULT); - persistedLogger = new EventBusPersistedLogger(cluster.environment().eventBus(), persistedLogWriter, merged); - }); - } - - - /** - * The main transactions 'engine', responsible for attempting the transaction logic as many times as required, - * until the transaction commits, is explicitly rolled back, or expires. - */ - // TODO: changed from private to public. package-protected plus an accessor would be ok to - public Mono executeTransaction(MergedTransactionConfig config, - TransactionContext overall, - Mono transactionLogic) { - AtomicReference startTime = new AtomicReference<>(); - - return Mono.just(overall) - - .subscribeOn(reactor.core.scheduler.Schedulers.elastic()) - - .doOnSubscribe(v -> { - if (startTime.get() == null) startTime.set(System.nanoTime()); - }) - - // Where the magic happens: execute the app's transaction logic - // A AttemptContextReactive gets created in here. Rollback requires one of these (so it knows what - // to rollback), so only errors thrown inside this block can trigger rollback. - // So, expiry checks only get done inside this block. - .then(transactionLogic) - - .flatMap(this::executeImplicitCommit) - - // Track an attempt if non-error, and request that the attempt be cleaned up. Similar logic is also - // done in executeHandleErrorsPreRetry. - .doOnNext(ctx -> executeAddAttemptAndCleanupRequest(config, overall, ctx)) - - // Track an attempt if error, and perform rollback if needed. - // All errors reaching here must be a `TransactionOperationFailed`. - .onErrorResume(err -> executeHandleErrorsPreRetry(config, overall, err)) - - // This is the main place to retry txns. Feed all errors up to this centralised point. - // All errors reaching here must be a `TransactionOperationFailed`. - .retryWhen(executeCreateRetryWhen(overall)) - - // If we're here, then we've hit an error that we don't want to retry. - // Either raise some derivative of TransactionFailed to the app, or return an AttemptContextReactive - // to return success (some errors result in success, e.g. TRANSACTION_FAILED_POST_COMMIT) - // All errors reaching here must be an `ErrorWrapper`. - .onErrorResume(err -> executeHandleErrorsPostRetry(overall, err)) - - .doOnError(err -> { - if (config.logOnFailure() && !config.logDirectly()) { - EventBus eventBus = cleanup.clusterData().cluster().environment().eventBus(); - overall.LOGGER.logs().forEach(log -> { - eventBus.publish(new TransactionLogEvent(config.logOnFailureLevel(), - TransactionLogEvent.DEFAULT_CATEGORY, log.toString())); - }); - } - }) - - // If we get here, success - .doOnSuccess(v -> - overall.LOGGER.info("finished txn in %dus", - TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startTime.get())) - ) - - // Safe to do single() as there will only ever be 1 result - .single() - .map(v -> createResultFromContext(overall)); - } - - private reactor.util.retry.Retry executeCreateRetryWhen(TransactionContext overall) { - Predicate> predicate = context -> { - Throwable exception = context.exception(); - - if (!(exception instanceof TransactionOperationFailed)) { - // A bug. Only TransactionOperationFailed is allowed to reach here. - throw new IllegalStateException("Non-TransactionOperationFailed '" + DebugUtil.dbg(exception) + "' received during retry, this is a bug", exception); - } - - TransactionOperationFailed e = (TransactionOperationFailed) exception; - - overall.LOGGER.info("TransactionOperationFailed retryTransaction=%s", e.retryTransaction()); - - return e.retryTransaction(); - }; - - return DefaultRetry.create(predicate) - - .exponentialBackoff(Duration.of(1, ChronoUnit.MILLIS), - Duration.of(2, ChronoUnit.MILLIS)) - - .doOnRetry(v -> overall.LOGGER.info("<>", "retrying transaction after backoff %dmillis", v.backoff().toMillis())) - - // Add some jitter so two txns don't livelock each other - .jitter(Jitter.random()) - - // Really, this is a safety-guard. The txn will be aborted when it expires. - .retryMax(MAX_ATTEMPTS) - - .toReactorRetry(); - } - - private Mono executeHandleErrorsPreRetry(MergedTransactionConfig config, - TransactionContext overall, Throwable err) { - if (!(err instanceof TransactionOperationFailed)) { - // A bug. Only TransactionOperationFailed is allowed to reach here. - overall.LOGGER.warn("<>", "received non-TransactionOperationFailed error %s, unable to rollback as don't have " + - "context", DebugUtil.dbg(err)); - return Mono.error(new IllegalStateException("received non-TransactionOperationFailed error " + err.getClass().getName() + " in pre-retry", err)); - } - - Mono autoRollback = Mono.empty(); - Mono cleanupReq = Mono.empty(); - - TransactionOperationFailed e = (TransactionOperationFailed) err; - AttemptContextReactive ctx = e.context(); - - overall.LOGGER.info("<>", "finishing attempt off after error '%s'", e); - - if (e.autoRollbackAttempt()) { - // In queryMode we always ROLLBACK, as there is possibly delta table state to cleanup, and there may be an - // ATR - we don't know - if (ctx.state() == AttemptStates.NOT_STARTED && !ctx.queryMode()) { - // This is a better way of doing [RETRY-ERR-NOATR] and likely means that the older logic for - // handling that won't trigger now - ctx.LOGGER.info(ctx.attemptId(), "told to auto-rollback but in NOT_STARTED state, so nothing to do - skipping rollback"); - } - else { - ctx.LOGGER.info(ctx.attemptId(), "auto-rolling-back on error"); - - autoRollback = ctx.rollbackInternal(false); - } - } else { - ctx.LOGGER.info(ctx.attemptId(), "has been told to skip auto-rollback"); - } - - if (!config.runRegularAttemptsCleanupThread()) { - // Don't add a request to a queue that no-one will be processing - ctx.LOGGER.trace(ctx.attemptId(), "skipping addition of cleanup request on failure as regular cleanup disabled"); - } - else { - cleanupReq = Mono.fromRunnable(() -> addCleanupRequestForContext(ctx)); - } - - Mono addAttempt = Mono.fromRunnable(() -> { - TransactionAttempt ta = TransactionAttempt.createFromContext(ctx, Optional.of(err)); - overall.addAttempt(ta); - ctx.LOGGER.info(ctx.attemptId(), "added attempt %s after error", ta); - }); - - final Mono cleanupReqForLambda = cleanupReq; - - return autoRollback - // See [Primary Operations] section in design document - .onErrorResume(er -> { - overall.LOGGER.info("<>", "rollback failed with %s, raising original error but with retryTransaction turned off", - DebugUtil.dbg(er)); - - // Still want to add attempt and cleanup request - return cleanupReqForLambda - .then(addAttempt) - .then(Mono.error(createError(e.context(), e.causingErrorClass()) - .raiseException(e.toRaise()) - .cause(e.getCause()) - .build())); - }) - .then(cleanupReqForLambda) - // Only want to add the attempt after doing the rollback, so the attempt has the correct state (hopefully - // ROLLED_BACK) - .then(addAttempt) - .then(Mono.defer(() -> { - if (e.retryTransaction() && overall.hasExpiredClientSide()) { - overall.LOGGER.info("<>", "original error planned to retry transaction, but it has subsequently expired"); - return Mono.error(createError(ctx, ErrorClasses.FAIL_EXPIRY) - .doNotRollbackAttempt() - .raiseException(TransactionOperationFailed.FinalErrorToRaise.TRANSACTION_EXPIRED) - .build()); - } - else { - // Raise the error up the stack so the logic later can decide whether to retry the transaction - overall.LOGGER.info("<>", "reraising original exception %s", DebugUtil.dbg(err)); - return Mono.error(err); - } - })) - .doFinally(v -> ctx.span().failWith(e)) - .thenReturn(ctx); - } - - private Mono executeHandleErrorsPostRetry(TransactionContext overall, Throwable err) { - if (!(err instanceof TransactionOperationFailed)) { - // A bug. Only TransactionOperationFailed is allowed to reach here. - return Mono.error(new IllegalStateException("Non-TransactionOperationFailed '" + DebugUtil.dbg(err) + "' received, this is a bug")); - } - - TransactionResult result = createResultFromContext(overall); - TransactionOperationFailed e = (TransactionOperationFailed) err; - - if (e.toRaise() == TransactionOperationFailed.FinalErrorToRaise.TRANSACTION_FAILED_POST_COMMIT) { - e.context().LOGGER.info(e.context().attemptId(), "converted TRANSACTION_FAILED_POST_COMMIT to success, unstagingComplete() will be false"); - - return Mono.just(e.context()); - } - else { - TransactionFailed ret; - - switch (e.toRaise()) { - case TRANSACTION_EXPIRED: { - String msg = "Transaction has expired configured timeout of " + overall.expirationTime().toMillis() + "msecs. The transaction is not committed."; - ret = new TransactionExpired(e.getCause(), result, msg); - break; - } - case TRANSACTION_COMMIT_AMBIGUOUS: { - String msg = "It is ambiguous whether the transaction committed"; - ret = new TransactionCommitAmbiguous(e.getCause(), result, msg); - break; - } - default: - ret = new TransactionFailed(e.getCause(), result); - break; - } - - e.context().LOGGER.info(e.context().attemptId(), "converted TransactionOperationFailed %s to final error %s", - e.toRaise(), ret); - - return Mono.error(ret); - } - } - - private void executeAddAttemptAndCleanupRequest(MergedTransactionConfig config, TransactionContext overall, - AttemptContextReactive ctx) { - TransactionAttempt ta = TransactionAttempt.createFromContext(ctx, Optional.empty()); - overall.addAttempt(ta); - ctx.LOGGER.info(ctx.attemptId(), "added attempt %s after success", ta); - - if (config.runRegularAttemptsCleanupThread()) { - addCleanupRequestForContext(ctx); - } else { - ctx.LOGGER.trace(ctx.attemptId(), "skipping addition of cleanup request on success"); - } - - ctx.span().finish(); - } - - private Mono executeImplicitCommit(AttemptContextReactive ctx) { - return Mono.defer(() -> { - // If app has not explicitly performed a commit, assume they want to do so anyway - if (!ctx.isDone()) { - if (ctx.serialized().isPresent()) { - return Mono.just(ctx); - } else { - ctx.LOGGER.trace(ctx.attemptId(), "doing implicit commit"); - - return ctx.commit() - .then(Mono.just(ctx)) - .onErrorResume(err -> Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx))); - } - } else { - return Mono.just(ctx); - } - }); - } - - // TODO: changed from package-protected to public (could have just used an accessor class in same package) - public AttemptContextReactive createAttemptContext(TransactionContext overall, - MergedTransactionConfig config, - String attemptId) { - // null only happens in testing with Mockito, harmless - if (overall != null) { - return attemptContextFactory.create(overall, config, attemptId, this, Optional.of(overall.span())); - } else { - return null; - } - } - - /** - * Runs the supplied transactional logic until success or failure. - *

    - * This is the asynchronous version of {@link Transactions#run}, so to cover the differences: - *

      - *
    • The transaction logic is supplied with a {@link AttemptContextReactive}, which contains asynchronous - * methods to allow it to read, mutate, insert and delete documents, as well as commit or rollback the - * transactions.
    • - *
    • The transaction logic should run these methods as a Reactor chain.
    • - *
    • The transaction logic should return a Mono{@literal <}Void{@literal >}. Any - * Flux or Mono can be converted to a Mono{@literal <}Void{@literal >} by - * calling .then() on it.
    • - *
    • This method returns a Mono{@literal <}TransactionResult{@literal >}, which should be handled - * as a normal Reactor Mono.
    • - *
    - * - * @param transactionLogic the application's transaction logic - * @param perConfig the configuration to use for this transaction - * @return there is no need to check the returned {@link TransactionResult}, as success is implied by the lack of a - * thrown exception. It contains information useful only for debugging and logging. - * @throws TransactionFailed or a derived exception if the transaction fails to commit for any reason, possibly - * after multiple retries. The exception contains further details of the error. Not - */ - public Mono run(Function> transactionLogic, - PerTransactionConfig perConfig) { - return Mono.defer(() -> { - MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); - - TransactionContext overall = - new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), - cleanup.clusterData().cluster().environment().eventBus(), - UUID.randomUUID().toString(), - now(), - Duration.ZERO, - merged); - AtomicReference startTime = new AtomicReference<>(0L); - - Mono ob = Mono.fromCallable(() -> { - String txnId = UUID.randomUUID().toString(); - overall.LOGGER.info(configDebug(config, perConfig)); - return createAttemptContext(overall, merged, txnId); - }).flatMap(ctx -> { - ctx.LOGGER.info("starting attempt %d/%s/%s", - overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); - Mono result = transactionLogic.apply(ctx); - return result - .onErrorResume(err -> { - ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); - logElidedStacktrace(ctx, err); - - return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); - }) - .thenReturn(ctx); - }).doOnSubscribe(v -> startTime.set(System.nanoTime())) - .doOnNext(v -> v.LOGGER.trace(v.attemptId(), "finished attempt %d in %sms", - overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); - - return executeTransaction(merged, overall, ob) - .doOnNext(v -> overall.span().finish()) - .doOnError(err -> overall.span().failWith(err)); - }); - } - - // Printing the stacktrace is expensive in terms of log noise, but has been a life saver on many debugging - // encounters. Strike a balance by eliding the more useless elements. - // TODO: changed from private to public - public void logElidedStacktrace(AttemptContextReactive ctx, Throwable err) { - DebugUtil.fetchElidedStacktrace(err, (s) -> ctx.LOGGER.info(ctx.attemptId(), " " + s.toString())); - } - - // TODO: changed from private to public - public static String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { - StringBuilder sb = new StringBuilder(); - sb.append("library version: "); - sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); - sb.append(" config: "); - sb.append("atrs="); - sb.append(config.numAtrs()); - sb.append(", metadataCollection="); - sb.append(config.metadataCollection()); - sb.append(", expiry="); - sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); - sb.append("msecs durability="); - sb.append(config.durabilityLevel()); - sb.append(" per-txn config="); - sb.append(" durability="); - sb.append(perConfig.durabilityLevel()); - sb.append(", supported="); - sb.append(Supported.SUPPORTED); - return sb.toString(); - } - - /** - * Convenience overload that runs {@link TransactionsReactive#run} with a default PerTransactionConfig. - */ - public Mono run(Function> transactionLogic) { - return run(transactionLogic, PerTransactionConfigBuilder.create().build()); - } - - @Stability.Volatile - public Mono commit(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { - return deferred(serialized, - perConfig, - // Nothing to actually do, just want the implicit commit - (ctx) -> Mono.empty()); - } - - @Stability.Volatile - public Mono rollback(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { - return deferred(serialized, - perConfig, - (ctx) -> ctx.rollback()); - } - - @Stability.Volatile - private Mono deferred(TransactionSerializedContext serialized, - PerTransactionConfig perConfig, - Function> initial) { - MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); - JsonObject hydrated = JsonObject.fromJson(serialized.encodeAsString()); - - String atrBucket = hydrated.getString("atrBucket"); - String atrScope = hydrated.getString("atrScope"); - String atrCollectionName = hydrated.getString("atrCollection"); - String atrId = hydrated.getString("atrId"); - ReactiveCollection atrCollection = cleanup.clusterData() - .getBucketFromName(atrBucket) - .scope(atrScope) - .collection(atrCollectionName); - - return ActiveTransactionRecord.getAtr(atrCollection, - atrId, - OptionsWrapperUtil.kvTimeoutNonMutating(merged, atrCollection.core()), - null) - - .flatMap(atrOpt -> { - if (!atrOpt.isPresent()) { - return Mono.error(new IllegalStateException(String.format("ATR %s/%s could not be found", - atrBucket, atrId))); - } - else { - ATR atr = atrOpt.get(); - - // Note startTimeServerMillis is written with ${Mutation.CAS} while currentTimeServer - // could have come from $vbucket.HLC and is hence one-second granularity. So, this is a - // somewhat imperfect comparison. - Duration currentTimeServer = Duration.ofNanos(atr.cas()); - Duration startTimeServer = Duration.ofMillis(hydrated.getLong("startTimeServerMillis")); - - // This includes the time elapsed during the first part of the transaction, plus any time - // elapsed during the period the transaction was expired. Total time since the transaction - // began, basically. - Duration timeElapsed = currentTimeServer.minus(startTimeServer); - - TransactionContext overall = - new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), - cleanup.clusterData().cluster().environment().eventBus(), - UUID.randomUUID().toString(), - Duration.ofNanos(System.nanoTime()), - timeElapsed, - merged); - AtomicReference startTime = new AtomicReference<>(0L); - - overall.LOGGER.info("elapsed time = %dmsecs (ATR start time %dmsecs, current ATR time %dmsecs)", - timeElapsed.toMillis(), startTimeServer.toMillis(), currentTimeServer.toMillis()); - - Mono ob = Mono.defer(() -> { - AttemptContextReactive ctx = attemptContextFactory.createFrom(hydrated, overall, merged, this); - ctx.LOGGER.info("starting attempt %d/%s/%s", - overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); - ctx.LOGGER.info(configDebug(config, perConfig)); - - return initial.apply(ctx) - - // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it - .subscribeOn(Schedulers.elastic()) - - .onErrorResume(err -> { - ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in deferred, rethrowing", - err); - - logElidedStacktrace(ctx, err); - - return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); - }) - - .doOnSubscribe(v -> startTime.set(System.nanoTime())) - - .doOnNext(v -> { - ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", - overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); - }) - - .thenReturn(ctx); - }); - - return executeTransaction(merged, overall, ob) - .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) - .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); - } - }); - } - - Mono runBlocking(Consumer txnLogic, PerTransactionConfig perConfig) { - return Mono.defer(() -> { - MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); - TransactionContext overall = - new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), - cleanup.clusterData().cluster().environment().eventBus(), - UUID.randomUUID().toString(), - now(), - Duration.ZERO, - merged); - AtomicReference startTime = new AtomicReference<>(0L); - overall.LOGGER.info(configDebug(config, perConfig)); - - Mono ob = Mono.defer(() -> { - String txnId = UUID.randomUUID().toString(); - AttemptContextReactive ctx = createAttemptContext(overall, merged, txnId); - AttemptContext ctxBlocking = new AttemptContext(ctx); - ctx.LOGGER.info("starting attempt %d/%s/%s", - overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); - - return Mono.fromRunnable(() -> txnLogic.accept(ctxBlocking)) - - // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it - .subscribeOn(Schedulers.elastic()) - - .onErrorResume(err -> { - ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in runBlocking, rethrowing", err); - - logElidedStacktrace(ctx, err); - - return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); - }) - - .doOnSubscribe(v -> startTime.set(System.nanoTime())) - - .doOnNext(v -> { - ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", - overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); - }) - - .thenReturn(ctx); - }); - - return executeTransaction(merged, overall, ob) - .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) - .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); - }); - } - - public TransactionConfig config() { - return config; - } - - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); - } - - TransactionsCleanup cleanup() { - return cleanup; - } - - private void addCleanupRequestForContext(AttemptContextReactive ctx) { - // Whether the txn was successful or not, still want to clean it up - if (ctx.queryMode()) { - ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as in query mode"); - } - else if (ctx.serialized().isPresent()) { - ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as deferred transaction"); - } - else if (ctx.atrId().isPresent() && ctx.atrCollection().isPresent()) { - switch (ctx.state()) { - case NOT_STARTED: - case COMPLETED: - case ROLLED_BACK: - ctx.LOGGER.trace(ctx.attemptId(), "Skipping addition of cleanup request in state %s", ctx.state()); - break; - default: - ctx.LOGGER.trace(ctx.attemptId(), "Adding cleanup request for %s/%s", - ctx.atrCollection().get().name(), ctx.atrId().get()); - - cleanup.add(ctx.createCleanupRequest()); - } - } else { - // No ATR entry to remove - ctx.LOGGER.trace(ctx.attemptId(), "Skipping cleanup request as no ATR entry to remove (due to no " + - "mutations)"); - } - } - - private static TransactionResult createResultFromContext(TransactionContext overall) { - return new TransactionResult(overall.attempts(), - overall.LOGGER, - Duration.of(System.nanoTime() - overall.startTimeClient().toNanos(), ChronoUnit.NANOS), - overall.transactionId(), - overall.serialized()); - } - - /** - * Performs a single query transaction, with default configuration. - * - * @param statement the statement to execute. - * @return a ReactiveSingleQueryTransactionResult - */ - @Stability.Uncommitted - public Mono query(String statement) { - return query(null, statement, SingleQueryTransactionConfigBuilder.create().build()); - } - - /** - * Performs a single query transaction, with a custom configuration. - * - * @param statement the statement to execute. - * @param queryOptions configuration options. - * @return a ReactiveSingleQueryTransactionResult - */ - @Stability.Uncommitted - public Mono query(String statement, SingleQueryTransactionConfig queryOptions) { - return query(null, statement, queryOptions); - } - - /** - * Performs a single query transaction, with a scope context and default configuration. - * - * @param statement the statement to execute. - * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope - * rather than needed to provide the full keyspace. - * @return a ReactiveSingleQueryTransactionResult - */ - @Stability.Uncommitted - public Mono query(ReactiveScope scope, String statement) { - return query(scope, statement, SingleQueryTransactionConfigBuilder.create().build()); - } - - /** - * Performs a single query transaction, with a scope context and custom configuration. - * - * @param statement the statement to execute. - * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope - * rather than needed to provide the full keyspace. - * @param queryOptions configuration options. - * @return a ReactiveSingleQueryTransactionResult - */ - @Stability.Uncommitted - public Mono query(ReactiveScope scope, String statement, SingleQueryTransactionConfig queryOptions) { - return Mono.defer(() -> { - AtomicReference queryResult = new AtomicReference<>(); - return run((ctx) -> ctx.query(scope, statement, queryOptions.queryOptions(), true) - .doOnNext(qr -> queryResult.set(qr)) - .then(), queryOptions.convert()) - .map(result -> new ReactiveSingleQueryTransactionResult(result.log(), queryResult.get())); - }); - } - - @Stability.Internal - @Deprecated // Prefer setting TransactionConfigBuilder#testFactories now - public void setAttemptContextFactory(AttemptContextFactory factory) { - this.attemptContextFactory = factory; - } - public AttemptContextReactive newAttemptContextReactive(){ - PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); - MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); - - TransactionContext overall = new TransactionContext( - cleanup().clusterData().cluster().environment().requestTracer(), - cleanup().clusterData().cluster().environment().eventBus(), - UUID.randomUUID().toString(), now(), Duration.ZERO, merged); - - String txnId = UUID.randomUUID().toString(); - overall.LOGGER.info(configDebug(config, perConfig)); - return createAttemptContext(overall, merged, txnId); - } - -} +///* +// * Copyright 2021 Couchbase, Inc. +// * +// * 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 +// * +// * http://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 com.couchbase.transactions; +// +//import com.couchbase.client.core.annotation.Stability; +//import com.couchbase.client.core.cnc.EventBus; +//import com.couchbase.client.core.retry.reactor.DefaultRetry; +//import com.couchbase.client.core.retry.reactor.Jitter; +//import com.couchbase.client.core.retry.reactor.RetryContext; +//import com.couchbase.client.java.Cluster; +//import com.couchbase.client.java.ReactiveCollection; +//import com.couchbase.client.java.ReactiveScope; +//import com.couchbase.client.java.json.JsonObject; +//import com.couchbase.client.java.query.ReactiveQueryResult; +//import com.couchbase.transactions.cleanup.ClusterData; +//import com.couchbase.transactions.cleanup.TransactionsCleanup; +//import com.couchbase.transactions.components.ATR; +//import com.couchbase.transactions.components.ActiveTransactionRecord; +//import com.couchbase.transactions.config.MergedTransactionConfig; +//import com.couchbase.transactions.config.PerTransactionConfig; +//import com.couchbase.transactions.config.PerTransactionConfigBuilder; +//import com.couchbase.transactions.config.SingleQueryTransactionConfig; +//import com.couchbase.transactions.config.SingleQueryTransactionConfigBuilder; +//import com.couchbase.transactions.config.TransactionConfig; +//import com.couchbase.transactions.deferred.TransactionSerializedContext; +//import com.couchbase.transactions.error.TransactionCommitAmbiguous; +//import com.couchbase.transactions.error.TransactionExpired; +//import com.couchbase.transactions.error.TransactionFailedException; +//import com.couchbase.transactions.error.internal.ErrorClasses; +//import com.couchbase.transactions.error.external.TransactionOperationFailed; +//import com.couchbase.transactions.forwards.Supported; +//import com.couchbase.transactions.log.EventBusPersistedLogger; +//import com.couchbase.transactions.log.PersistedLogWriter; +//import com.couchbase.transactions.log.TransactionLogEvent; +//import com.couchbase.transactions.support.AttemptContextFactory; +//import com.couchbase.transactions.support.AttemptState; +//import com.couchbase.transactions.support.OptionsWrapperUtil; +//import com.couchbase.transactions.util.DebugUtil; +//import reactor.core.publisher.Mono; +//import reactor.core.scheduler.Schedulers; +// +//import java.time.Duration; +//import java.time.temporal.ChronoUnit; +//import java.util.Objects; +//import java.util.Optional; +//import java.util.UUID; +//import java.util.concurrent.TimeUnit; +//import java.util.concurrent.atomic.AtomicReference; +//import java.util.function.Consumer; +//import java.util.function.Function; +//import java.util.function.Predicate; +// +//import static com.couchbase.transactions.error.internal.TransactionOperationFailedBuilder.createError; +//import static com.couchbase.transactions.log.PersistedLogWriter.MAX_LOG_ENTRIES_DEFAULT; +//import static com.couchbase.transactions.support.SpanWrapperUtil.DB_COUCHBASE_TRANSACTIONS; +// +///** +// * An asynchronous version of {@link Transactions}, allowing transactions to be created and run in an asynchronous +// * manner. +// *

    +// * The main method to run transactions is {@link TransactionsReactive#run}. +// */ +//public class TransactionsReactive { +// static final int MAX_ATTEMPTS = 1000; +// private final TransactionsCleanup cleanup; +// private final TransactionConfig config; +// private AttemptContextFactory attemptContextFactory; +// private EventBusPersistedLogger persistedLogger; +// +// /** +// * This is package-private. Applications should create a {@link Transactions} object instead, and then call {@link +// * Transactions#reactive}. +// */ +// static TransactionsReactive create(Cluster cluster, TransactionConfig config) { +// return new TransactionsReactive(cluster, config); +// } +// +// private TransactionsReactive(Cluster cluster, TransactionConfig config) { +// Objects.requireNonNull(cluster); +// Objects.requireNonNull(config); +// +// ClusterData clusterData = new ClusterData(cluster); +// this.config = config; +// this.attemptContextFactory = config.attemptContextFactory(); +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.empty()); +// cleanup = new TransactionsCleanup(merged, clusterData); +// +// config.persistentLoggingCollection().ifPresent(collection -> { +// PersistedLogWriter persistedLogWriter = new PersistedLogWriter(collection, MAX_LOG_ENTRIES_DEFAULT); +// persistedLogger = new EventBusPersistedLogger(cluster.environment().eventBus(), persistedLogWriter, merged); +// }); +// } +// +// +// /** +// * The main transactions 'engine', responsible for attempting the transaction logic as many times as required, +// * until the transaction commits, is explicitly rolled back, or expires. +// */ +// // TODO: changed from private to public. package-protected plus an accessor would be ok to +// public Mono executeTransaction(MergedTransactionConfig config, +// TransactionContext overall, +// Mono transactionLogic) { +// AtomicReference startTime = new AtomicReference<>(); +// +// return Mono.just(overall) +// +// .subscribeOn(reactor.core.scheduler.Schedulers.elastic()) +// +// .doOnSubscribe(v -> { +// if (startTime.get() == null) startTime.set(System.nanoTime()); +// }) +// +// // Where the magic happens: execute the app's transaction logic +// // A ReactiveTransactionAttemptContext gets created in here. Rollback requires one of these (so it knows what +// // to rollback), so only errors thrown inside this block can trigger rollback. +// // So, expiry checks only get done inside this block. +// .then(transactionLogic) +// +// .flatMap(this::executeImplicitCommit) +// +// // Track an attempt if non-error, and request that the attempt be cleaned up. Similar logic is also +// // done in executeHandleErrorsPreRetry. +// .doOnNext(ctx -> executeAddAttemptAndCleanupRequest(config, overall, ctx)) +// +// // Track an attempt if error, and perform rollback if needed. +// // All errors reaching here must be a `TransactionOperationFailed`. +// .onErrorResume(err -> executeHandleErrorsPreRetry(config, overall, err)) +// +// // This is the main place to retry txns. Feed all errors up to this centralised point. +// // All errors reaching here must be a `TransactionOperationFailed`. +// .retryWhen(executeCreateRetryWhen(overall)) +// +// // If we're here, then we've hit an error that we don't want to retry. +// // Either raise some derivative of TransactionFailedException to the app, or return an ReactiveTransactionAttemptContext +// // to return success (some errors result in success, e.g. TRANSACTION_FAILED_POST_COMMIT) +// // All errors reaching here must be an `ErrorWrapper`. +// .onErrorResume(err -> executeHandleErrorsPostRetry(overall, err)) +// +// .doOnError(err -> { +// if (config.logOnFailure() && !config.logDirectly()) { +// EventBus eventBus = cleanup.clusterData().cluster().environment().eventBus(); +// overall.LOGGER.logs().forEach(log -> { +// eventBus.publish(new TransactionLogEvent(config.logOnFailureLevel(), +// TransactionLogEvent.DEFAULT_CATEGORY, log.toString())); +// }); +// } +// }) +// +// // If we get here, success +// .doOnSuccess(v -> +// overall.LOGGER.info("finished txn in %dus", +// TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startTime.get())) +// ) +// +// // Safe to do single() as there will only ever be 1 result +// .single() +// .map(v -> createResultFromContext(overall)); +// } +// +// private reactor.util.retry.Retry executeCreateRetryWhen(TransactionContext overall) { +// Predicate> predicate = context -> { +// Throwable exception = context.exception(); +// +// if (!(exception instanceof TransactionOperationFailed)) { +// // A bug. Only TransactionOperationFailed is allowed to reach here. +// throw new IllegalStateException("Non-TransactionOperationFailed '" + DebugUtil.dbg(exception) + "' received during retry, this is a bug", exception); +// } +// +// TransactionOperationFailed e = (TransactionOperationFailed) exception; +// +// overall.LOGGER.info("TransactionOperationFailed retryTransaction=%s", e.retryTransaction()); +// +// return e.retryTransaction(); +// }; +// +// return DefaultRetry.create(predicate) +// +// .exponentialBackoff(Duration.of(1, ChronoUnit.MILLIS), +// Duration.of(2, ChronoUnit.MILLIS)) +// +// .doOnRetry(v -> overall.LOGGER.info("<>", "retrying transaction after backoff %dmillis", v.backoff().toMillis())) +// +// // Add some jitter so two txns don't livelock each other +// .jitter(Jitter.random()) +// +// // Really, this is a safety-guard. The txn will be aborted when it expires. +// .retryMax(MAX_ATTEMPTS) +// +// .toReactorRetry(); +// } +// +// private Mono executeHandleErrorsPreRetry(MergedTransactionConfig config, +// TransactionContext overall, Throwable err) { +// if (!(err instanceof TransactionOperationFailed)) { +// // A bug. Only TransactionOperationFailed is allowed to reach here. +// overall.LOGGER.warn("<>", "received non-TransactionOperationFailed error %s, unable to rollback as don't have " + +// "context", DebugUtil.dbg(err)); +// return Mono.error(new IllegalStateException("received non-TransactionOperationFailed error " + err.getClass().getName() + " in pre-retry", err)); +// } +// +// Mono autoRollback = Mono.empty(); +// Mono cleanupReq = Mono.empty(); +// +// TransactionOperationFailed e = (TransactionOperationFailed) err; +// ReactiveTransactionAttemptContext ctx = e.context(); +// +// overall.LOGGER.info("<>", "finishing attempt off after error '%s'", e); +// +// if (e.autoRollbackAttempt()) { +// // In queryMode we always ROLLBACK, as there is possibly delta table state to cleanup, and there may be an +// // ATR - we don't know +// if (ctx.state() == AttemptState.NOT_STARTED && !ctx.queryMode()) { +// // This is a better way of doing [RETRY-ERR-NOATR] and likely means that the older logic for +// // handling that won't trigger now +// ctx.LOGGER.info(ctx.attemptId(), "told to auto-rollback but in NOT_STARTED state, so nothing to do - skipping rollback"); +// } +// else { +// ctx.LOGGER.info(ctx.attemptId(), "auto-rolling-back on error"); +// +// autoRollback = ctx.rollbackInternal(false); +// } +// } else { +// ctx.LOGGER.info(ctx.attemptId(), "has been told to skip auto-rollback"); +// } +// +// if (!config.runRegularAttemptsCleanupThread()) { +// // Don't add a request to a queue that no-one will be processing +// ctx.LOGGER.trace(ctx.attemptId(), "skipping addition of cleanup request on failure as regular cleanup disabled"); +// } +// else { +// cleanupReq = Mono.fromRunnable(() -> addCleanupRequestForContext(ctx)); +// } +// +// Mono addAttempt = Mono.fromRunnable(() -> { +// TransactionAttempt ta = TransactionAttempt.createFromContext(ctx, Optional.of(err)); +// overall.addAttempt(ta); +// ctx.LOGGER.info(ctx.attemptId(), "added attempt %s after error", ta); +// }); +// +// final Mono cleanupReqForLambda = cleanupReq; +// +// return autoRollback +// // See [Primary Operations] section in design document +// .onErrorResume(er -> { +// overall.LOGGER.info("<>", "rollback failed with %s, raising original error but with retryTransaction turned off", +// DebugUtil.dbg(er)); +// +// // Still want to add attempt and cleanup request +// return cleanupReqForLambda +// .then(addAttempt) +// .then(Mono.error(createError(e.context(), e.causingErrorClass()) +// .raiseException(e.toRaise()) +// .cause(e.getCause()) +// .build())); +// }) +// .then(cleanupReqForLambda) +// // Only want to add the attempt after doing the rollback, so the attempt has the correct state (hopefully +// // ROLLED_BACK) +// .then(addAttempt) +// .then(Mono.defer(() -> { +// if (e.retryTransaction() && overall.hasExpiredClientSide()) { +// overall.LOGGER.info("<>", "original error planned to retry transaction, but it has subsequently expired"); +// return Mono.error(createError(ctx, ErrorClasses.FAIL_EXPIRY) +// .doNotRollbackAttempt() +// .raiseException(TransactionOperationFailed.FinalErrorToRaise.TRANSACTION_EXPIRED) +// .build()); +// } +// else { +// // Raise the error up the stack so the logic later can decide whether to retry the transaction +// overall.LOGGER.info("<>", "reraising original exception %s", DebugUtil.dbg(err)); +// return Mono.error(err); +// } +// })) +// .doFinally(v -> ctx.span().failWith(e)) +// .thenReturn(ctx); +// } +// +// private Mono executeHandleErrorsPostRetry(TransactionContext overall, Throwable err) { +// if (!(err instanceof TransactionOperationFailed)) { +// // A bug. Only TransactionOperationFailed is allowed to reach here. +// return Mono.error(new IllegalStateException("Non-TransactionOperationFailed '" + DebugUtil.dbg(err) + "' received, this is a bug")); +// } +// +// TransactionResult result = createResultFromContext(overall); +// TransactionOperationFailed e = (TransactionOperationFailed) err; +// +// if (e.toRaise() == TransactionOperationFailed.FinalErrorToRaise.TRANSACTION_FAILED_POST_COMMIT) { +// e.context().LOGGER.info(e.context().attemptId(), "converted TRANSACTION_FAILED_POST_COMMIT to success, unstagingComplete() will be false"); +// +// return Mono.just(e.context()); +// } +// else { +// TransactionFailedException ret; +// +// switch (e.toRaise()) { +// case TRANSACTION_EXPIRED: { +// String msg = "Transaction has expired configured timeout of " + overall.expirationTime().toMillis() + "msecs. The transaction is not committed."; +// ret = new TransactionExpired(e.getCause(), result, msg); +// break; +// } +// case TRANSACTION_COMMIT_AMBIGUOUS: { +// String msg = "It is ambiguous whether the transaction committed"; +// ret = new TransactionCommitAmbiguous(e.getCause(), result, msg); +// break; +// } +// default: +// ret = new TransactionFailedException(e.getCause(), result); +// break; +// } +// +// e.context().LOGGER.info(e.context().attemptId(), "converted TransactionOperationFailed %s to final error %s", +// e.toRaise(), ret); +// +// return Mono.error(ret); +// } +// } +// +// private void executeAddAttemptAndCleanupRequest(MergedTransactionConfig config, TransactionContext overall, +// ReactiveTransactionAttemptContext ctx) { +// TransactionAttempt ta = TransactionAttempt.createFromContext(ctx, Optional.empty()); +// overall.addAttempt(ta); +// ctx.LOGGER.info(ctx.attemptId(), "added attempt %s after success", ta); +// +// if (config.runRegularAttemptsCleanupThread()) { +// addCleanupRequestForContext(ctx); +// } else { +// ctx.LOGGER.trace(ctx.attemptId(), "skipping addition of cleanup request on success"); +// } +// +// ctx.span().finish(); +// } +// +// private Mono executeImplicitCommit(ReactiveTransactionAttemptContext ctx) { +// return Mono.defer(() -> { +// // If app has not explicitly performed a commit, assume they want to do so anyway +// if (!ctx.isDone()) { +// if (ctx.serialized().isPresent()) { +// return Mono.just(ctx); +// } else { +// ctx.LOGGER.trace(ctx.attemptId(), "doing implicit commit"); +// +// return ctx.commit() +// .then(Mono.just(ctx)) +// .onErrorResume(err -> Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx))); +// } +// } else { +// return Mono.just(ctx); +// } +// }); +// } +// +// // TODO: changed from package-protected to public (could have just used an accessor class in same package) +// public ReactiveTransactionAttemptContext createAttemptContext(TransactionContext overall, +// MergedTransactionConfig config, +// String attemptId) { +// // null only happens in testing with Mockito, harmless +// if (overall != null) { +// return attemptContextFactory.create(overall, config, attemptId, this, Optional.of(overall.span())); +// } else { +// return null; +// } +// } +// +// /** +// * Runs the supplied transactional logic until success or failure. +// *

    +// * This is the asynchronous version of {@link Transactions#run}, so to cover the differences: +// *

      +// *
    • The transaction logic is supplied with a {@link ReactiveTransactionAttemptContext}, which contains asynchronous +// * methods to allow it to read, mutate, insert and delete documents, as well as commit or rollback the +// * transactions.
    • +// *
    • The transaction logic should run these methods as a Reactor chain.
    • +// *
    • The transaction logic should return a Mono{@literal <}Void{@literal >}. Any +// * Flux or Mono can be converted to a Mono{@literal <}Void{@literal >} by +// * calling .then() on it.
    • +// *
    • This method returns a Mono{@literal <}TransactionResult{@literal >}, which should be handled +// * as a normal Reactor Mono.
    • +// *
    +// * +// * @param transactionLogic the application's transaction logic +// * @param perConfig the configuration to use for this transaction +// * @return there is no need to check the returned {@link TransactionResult}, as success is implied by the lack of a +// * thrown exception. It contains information useful only for debugging and logging. +// * @throws TransactionFailedException or a derived exception if the transaction fails to commit for any reason, possibly +// * after multiple retries. The exception contains further details of the error. Not +// */ +// public Mono run(Function> transactionLogic, +// PerTransactionConfig perConfig) { +// return Mono.defer(() -> { +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// +// TransactionContext overall = +// new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), +// cleanup.clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), +// now(), +// Duration.ZERO, +// merged); +// AtomicReference startTime = new AtomicReference<>(0L); +// +// Mono ob = Mono.fromCallable(() -> { +// String txnId = UUID.randomUUID().toString(); +// overall.LOGGER.info(configDebug(config, perConfig)); +// return createAttemptContext(overall, merged, txnId); +// }).flatMap(ctx -> { +// ctx.LOGGER.info("starting attempt %d/%s/%s", +// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); +// Mono result = transactionLogic.apply(ctx); +// return result +// .onErrorResume(err -> { +// ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); +// logElidedStacktrace(ctx, err); +// +// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); +// }) +// .thenReturn(ctx); +// }).doOnSubscribe(v -> startTime.set(System.nanoTime())) +// .doOnNext(v -> v.LOGGER.trace(v.attemptId(), "finished attempt %d in %sms", +// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); +// +// return executeTransaction(merged, overall, ob) +// .doOnNext(v -> overall.span().finish()) +// .doOnError(err -> overall.span().failWith(err)); +// }); +// } +// +// // Printing the stacktrace is expensive in terms of log noise, but has been a life saver on many debugging +// // encounters. Strike a balance by eliding the more useless elements. +// // TODO: changed from private to public +// public void logElidedStacktrace(ReactiveTransactionAttemptContext ctx, Throwable err) { +// DebugUtil.fetchElidedStacktrace(err, (s) -> ctx.LOGGER.info(ctx.attemptId(), " " + s.toString())); +// } +// +// // TODO: changed from private to public +// public static String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { +// StringBuilder sb = new StringBuilder(); +// sb.append("library version: "); +// sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); +// sb.append(" config: "); +// sb.append("atrs="); +// sb.append(config.numAtrs()); +// sb.append(", metadataCollection="); +// sb.append(config.metadataCollection()); +// sb.append(", expiry="); +// sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); +// sb.append("msecs durability="); +// sb.append(config.durabilityLevel()); +// sb.append(" per-txn config="); +// sb.append(" durability="); +// sb.append(perConfig.durabilityLevel()); +// sb.append(", supported="); +// sb.append(Supported.SUPPORTED); +// return sb.toString(); +// } +// +// /** +// * Convenience overload that runs {@link TransactionsReactive#run} with a default PerTransactionConfig. +// */ +// public Mono run(Function> transactionLogic) { +// return run(transactionLogic, PerTransactionConfigBuilder.create().build()); +// } +// +// @Stability.Volatile +// public Mono commit(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { +// return deferred(serialized, +// perConfig, +// // Nothing to actually do, just want the implicit commit +// (ctx) -> Mono.empty()); +// } +// +// @Stability.Volatile +// public Mono rollback(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { +// return deferred(serialized, +// perConfig, +// (ctx) -> ctx.rollback()); +// } +// +// @Stability.Volatile +// private Mono deferred(TransactionSerializedContext serialized, +// PerTransactionConfig perConfig, +// Function> initial) { +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// JsonObject hydrated = JsonObject.fromJson(serialized.encodeAsString()); +// +// String atrBucket = hydrated.getString("atrBucket"); +// String atrScope = hydrated.getString("atrScope"); +// String atrCollectionName = hydrated.getString("atrCollection"); +// String atrId = hydrated.getString("atrId"); +// ReactiveCollection atrCollection = cleanup.clusterData() +// .getBucketFromName(atrBucket) +// .scope(atrScope) +// .collection(atrCollectionName); +// +// return ActiveTransactionRecord.getAtr(atrCollection, +// atrId, +// OptionsWrapperUtil.kvTimeoutNonMutating(merged, atrCollection.core()), +// null) +// +// .flatMap(atrOpt -> { +// if (!atrOpt.isPresent()) { +// return Mono.error(new IllegalStateException(String.format("ATR %s/%s could not be found", +// atrBucket, atrId))); +// } +// else { +// ATR atr = atrOpt.get(); +// +// // Note startTimeServerMillis is written with ${Mutation.CAS} while currentTimeServer +// // could have come from $vbucket.HLC and is hence one-second granularity. So, this is a +// // somewhat imperfect comparison. +// Duration currentTimeServer = Duration.ofNanos(atr.cas()); +// Duration startTimeServer = Duration.ofMillis(hydrated.getLong("startTimeServerMillis")); +// +// // This includes the time elapsed during the first part of the transaction, plus any time +// // elapsed during the period the transaction was expired. Total time since the transaction +// // began, basically. +// Duration timeElapsed = currentTimeServer.minus(startTimeServer); +// +// TransactionContext overall = +// new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), +// cleanup.clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), +// Duration.ofNanos(System.nanoTime()), +// timeElapsed, +// merged); +// AtomicReference startTime = new AtomicReference<>(0L); +// +// overall.LOGGER.info("elapsed time = %dmsecs (ATR start time %dmsecs, current ATR time %dmsecs)", +// timeElapsed.toMillis(), startTimeServer.toMillis(), currentTimeServer.toMillis()); +// +// Mono ob = Mono.defer(() -> { +// ReactiveTransactionAttemptContext ctx = attemptContextFactory.createFrom(hydrated, overall, merged, this); +// ctx.LOGGER.info("starting attempt %d/%s/%s", +// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); +// ctx.LOGGER.info(configDebug(config, perConfig)); +// +// return initial.apply(ctx) +// +// // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it +// .subscribeOn(Schedulers.elastic()) +// +// .onErrorResume(err -> { +// ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in deferred, rethrowing", +// err); +// +// logElidedStacktrace(ctx, err); +// +// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); +// }) +// +// .doOnSubscribe(v -> startTime.set(System.nanoTime())) +// +// .doOnNext(v -> { +// ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", +// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); +// }) +// +// .thenReturn(ctx); +// }); +// +// return executeTransaction(merged, overall, ob) +// .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) +// .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); +// } +// }); +// } +// +// Mono runBlocking(Consumer txnLogic, PerTransactionConfig perConfig) { +// return Mono.defer(() -> { +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// TransactionContext overall = +// new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), +// cleanup.clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), +// now(), +// Duration.ZERO, +// merged); +// AtomicReference startTime = new AtomicReference<>(0L); +// overall.LOGGER.info(configDebug(config, perConfig)); +// +// Mono ob = Mono.defer(() -> { +// String txnId = UUID.randomUUID().toString(); +// ReactiveTransactionAttemptContext ctx = createAttemptContext(overall, merged, txnId); +// TransactionAttemptContext ctxBlocking = new TransactionAttemptContext(ctx); +// ctx.LOGGER.info("starting attempt %d/%s/%s", +// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); +// +// return Mono.fromRunnable(() -> txnLogic.accept(ctxBlocking)) +// +// // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it +// .subscribeOn(Schedulers.elastic()) +// +// .onErrorResume(err -> { +// ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in runBlocking, rethrowing", err); +// +// logElidedStacktrace(ctx, err); +// +// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); +// }) +// +// .doOnSubscribe(v -> startTime.set(System.nanoTime())) +// +// .doOnNext(v -> { +// ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", +// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); +// }) +// +// .thenReturn(ctx); +// }); +// +// return executeTransaction(merged, overall, ob) +// .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) +// .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); +// }); +// } +// +// public TransactionConfig config() { +// return config; +// } +// +// private static Duration now() { +// return Duration.of(System.nanoTime(), ChronoUnit.NANOS); +// } +// +// TransactionsCleanup cleanup() { +// return cleanup; +// } +// +// private void addCleanupRequestForContext(ReactiveTransactionAttemptContext ctx) { +// // Whether the txn was successful or not, still want to clean it up +// if (ctx.queryMode()) { +// ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as in query mode"); +// } +// else if (ctx.serialized().isPresent()) { +// ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as deferred transaction"); +// } +// else if (ctx.atrId().isPresent() && ctx.atrCollection().isPresent()) { +// switch (ctx.state()) { +// case NOT_STARTED: +// case COMPLETED: +// case ROLLED_BACK: +// ctx.LOGGER.trace(ctx.attemptId(), "Skipping addition of cleanup request in state %s", ctx.state()); +// break; +// default: +// ctx.LOGGER.trace(ctx.attemptId(), "Adding cleanup request for %s/%s", +// ctx.atrCollection().get().name(), ctx.atrId().get()); +// +// cleanup.add(ctx.createCleanupRequest()); +// } +// } else { +// // No ATR entry to remove +// ctx.LOGGER.trace(ctx.attemptId(), "Skipping cleanup request as no ATR entry to remove (due to no " + +// "mutations)"); +// } +// } +// +// private static TransactionResult createResultFromContext(TransactionContext overall) { +// return new TransactionResult(overall.attempts(), +// overall.LOGGER, +// Duration.of(System.nanoTime() - overall.startTimeClient().toNanos(), ChronoUnit.NANOS), +// overall.transactionId(), +// overall.serialized()); +// } +// +// /** +// * Performs a single query transaction, with default configuration. +// * +// * @param statement the statement to execute. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(String statement) { +// return query(null, statement, SingleQueryTransactionConfigBuilder.create().build()); +// } +// +// /** +// * Performs a single query transaction, with a custom configuration. +// * +// * @param statement the statement to execute. +// * @param queryOptions configuration options. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(String statement, SingleQueryTransactionConfig queryOptions) { +// return query(null, statement, queryOptions); +// } +// +// /** +// * Performs a single query transaction, with a scope context and default configuration. +// * +// * @param statement the statement to execute. +// * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope +// * rather than needed to provide the full keyspace. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(ReactiveScope scope, String statement) { +// return query(scope, statement, SingleQueryTransactionConfigBuilder.create().build()); +// } +// +// /** +// * Performs a single query transaction, with a scope context and custom configuration. +// * +// * @param statement the statement to execute. +// * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope +// * rather than needed to provide the full keyspace. +// * @param queryOptions configuration options. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(ReactiveScope scope, String statement, SingleQueryTransactionConfig queryOptions) { +// return Mono.defer(() -> { +// AtomicReference queryResult = new AtomicReference<>(); +// return run((ctx) -> ctx.query(scope, statement, queryOptions.queryOptions(), true) +// .doOnNext(qr -> queryResult.set(qr)) +// .then(), queryOptions.convert()) +// .map(result -> new ReactiveSingleQueryTransactionResult(result.log(), queryResult.get())); +// }); +// } +// +// @Stability.Internal +// @Deprecated // Prefer setting TransactionConfigBuilder#testFactories now +// public void setAttemptContextFactory(AttemptContextFactory factory) { +// this.attemptContextFactory = factory; +// } +// public ReactiveTransactionAttemptContext newAttemptContextReactive(){ +// PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// +// TransactionContext overall = new TransactionContext( +// cleanup().clusterData().cluster().environment().requestTracer(), +// cleanup().clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), now(), Duration.ZERO, merged); +// +// String txnId = UUID.randomUUID().toString(); +// overall.LOGGER.info(configDebug(config, perConfig)); +// return createAttemptContext(overall, merged, txnId); +// } +// +//} diff --git a/src/main/java/com/example/demo/CouchbaseTransactionManager.java b/src/main/java/com/example/demo/CouchbaseTransactionManager.java index 41f62bf26..967fd06fd 100644 --- a/src/main/java/com/example/demo/CouchbaseTransactionManager.java +++ b/src/main/java/com/example/demo/CouchbaseTransactionManager.java @@ -2,7 +2,9 @@ import java.util.concurrent.atomic.AtomicReference; -import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionResult; import com.couchbase.transactions.AttemptContextReactiveAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,25 +26,17 @@ import org.springframework.transaction.support.TransactionSynchronizationUtils; import org.springframework.util.Assert; -import com.couchbase.transactions.AttemptContext; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; - +// todo gp why is there separate CouchbaseCallbackTransactionManager if this class also extends CallbackPreferringPlatformTransactionManager? +// todo gp there is another CouchbaseTransactionManager in another package, which is valid? public class CouchbaseTransactionManager extends AbstractPlatformTransactionManager implements DisposableBean, ResourceTransactionManager, CallbackPreferringPlatformTransactionManager { private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); private final CouchbaseTemplate template; - private final Transactions transactions; - public CouchbaseTransactionManager(CouchbaseTemplate template, TransactionConfig transactionConfig) { + public CouchbaseTransactionManager(CouchbaseTemplate template) { this.template = template; - this.transactions = Transactions.create( - template.getCouchbaseClientFactory().getCluster(), - transactionConfig - ); } public CouchbaseTransactionalTemplate template() { @@ -52,35 +46,36 @@ public CouchbaseTransactionalTemplate template() { @Override public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { final AtomicReference result = new AtomicReference<>(); - TransactionResult txnResult = transactions.run(attemptContext -> { - - if (TransactionSynchronizationManager.hasResource(template.getCouchbaseClientFactory())) { - ((CouchbaseResourceHolder) TransactionSynchronizationManager - .getResource(template.reactive().getCouchbaseClientFactory())) - .setAttemptContext(attemptContext); - } else { - TransactionSynchronizationManager.bindResource( - template.reactive().getCouchbaseClientFactory(), - new CouchbaseResourceHolder(attemptContext) - ); - } - - try { - // Since we are on a different thread now transparently, at least make sure - // that the original method invocation is synchronized. - synchronized (this) { - result.set(callback.doInTransaction(null)); - } - } catch (RuntimeException e) { - System.err.println("RuntimeException: "+e+" instanceof RuntimeException "+(e instanceof RuntimeException)); - throw e; - } catch (Throwable e) { - System.err.println("RuntimeException: "+e+" instanceof "+(e instanceof Throwable)); - throw new RuntimeException(e); - } - }); - - LOGGER.debug("Completed Couchbase Transaction with Result: " + txnResult); + // todo gp like CouchbaseCallbackTransactionManager, it needs access to CouchbaseClientFactory here (Cluster) +// TransactionResult txnResult = transactions.run(attemptContext -> { +// +// if (TransactionSynchronizationManager.hasResource(template.getCouchbaseClientFactory())) { +// ((CouchbaseResourceHolder) TransactionSynchronizationManager +// .getResource(template.reactive().getCouchbaseClientFactory())) +// .setAttemptContext(attemptContext); +// } else { +// TransactionSynchronizationManager.bindResource( +// template.reactive().getCouchbaseClientFactory(), +// new CouchbaseResourceHolder(attemptContext) +// ); +// } +// +// try { +// // Since we are on a different thread now transparently, at least make sure +// // that the original method invocation is synchronized. +// synchronized (this) { +// result.set(callback.doInTransaction(null)); +// } +// } catch (RuntimeException e) { +// System.err.println("RuntimeException: "+e+" instanceof RuntimeException "+(e instanceof RuntimeException)); +// throw e; +// } catch (Throwable e) { +// System.err.println("RuntimeException: "+e+" instanceof "+(e instanceof Throwable)); +// throw new RuntimeException(e); +// } +// }); + +// LOGGER.debug("Completed Couchbase Transaction with Result: " + txnResult); return result.get(); } @@ -118,7 +113,6 @@ protected void doCleanupAfterCompletion(Object transaction) { @Override public void destroy() { - transactions.close(); } @Override @@ -136,26 +130,26 @@ private static CouchbaseTransactionObject extractTransaction(Object transaction) public static class CouchbaseResourceHolder extends ResourceHolderSupport { - private volatile AttemptContext attemptContext; - private volatile AttemptContextReactive attemptContextReactive; + private volatile TransactionAttemptContext attemptContext; + private volatile ReactiveTransactionAttemptContext attemptContextReactive; private volatile ClientSession session = new ClientSessionImpl(); - public CouchbaseResourceHolder(AttemptContext attemptContext) { + public CouchbaseResourceHolder(TransactionAttemptContext attemptContext) { this.attemptContext = attemptContext; } - public AttemptContext getAttemptContext() { + public TransactionAttemptContext getAttemptContext() { return attemptContext; } - public void setAttemptContext(AttemptContext attemptContext) { + public void setAttemptContext(TransactionAttemptContext attemptContext) { this.attemptContext = attemptContext; } - public AttemptContextReactive getAttemptContextReactive() { + public ReactiveTransactionAttemptContext getAttemptContextReactive() { return attemptContext!= null ? AttemptContextReactiveAccessor.getACR(attemptContext) : attemptContextReactive; } - public void setAttemptContextReactive(AttemptContextReactive attemptContextReactive) { + public void setAttemptContextReactive(ReactiveTransactionAttemptContext attemptContextReactive) { this.attemptContextReactive = attemptContextReactive; } diff --git a/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java index 70116952c..4fa7cecfc 100644 --- a/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java +++ b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java @@ -1,15 +1,14 @@ package com.example.demo; -import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; -import com.couchbase.transactions.AttemptContext; -import com.couchbase.transactions.TransactionGetResult; - public class CouchbaseTransactionalTemplate { private final CouchbaseTemplate template; @@ -20,10 +19,11 @@ public CouchbaseTransactionalTemplate(CouchbaseTemplate template) { public SpringTransactionGetResult findById(String id, Class domainType) { try { - AttemptContext ctx = getContext(); + TransactionAttemptContext ctx = getContext(); TransactionGetResult getResult = ctx.get(template.getCouchbaseClientFactory().getDefaultCollection(), id); - T t = template.support().decodeEntity(id, getResult.contentAsObject().toString(), getResult.cas(), domainType, + // todo gp getResult.cas() is no longer exposed - required? + T t = template.support().decodeEntity(id, getResult.contentAsObject().toString(), 0, domainType, null, null, null); return new SpringTransactionGetResult<>(t, getResult); } catch (Exception e) { @@ -34,36 +34,36 @@ public SpringTransactionGetResult findById(String id, Class domainType } public void replaceById(TransactionGetResult getResult, T entity) { - AttemptContext ctx = getContext(); + TransactionAttemptContext ctx = getContext(); ctx.replace(getResult, template.support().encodeEntity(entity).getContent()); } - private AttemptContext getContext() { + private TransactionAttemptContext getContext() { CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager .getResource(template.getCouchbaseClientFactory()); - AttemptContext atr; + TransactionAttemptContext atr; if (resource != null) { atr = resource.getAttemptContext(); } else { CouchbaseResourceHolder holder = (CouchbaseResourceHolder) TransactionSynchronizationManager .getResource(template.getCouchbaseClientFactory().getCluster()); - atr = holder.getSession().getAttemptContext(); + atr = holder.getSession().getTransactionAttemptContext(); } return atr; } - public static AttemptContextReactive getContextReactive(ReactiveCouchbaseTemplate template) { + public static ReactiveTransactionAttemptContext getContextReactive(ReactiveCouchbaseTemplate template) { CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager .getResource(template.getCouchbaseClientFactory()); - AttemptContextReactive atr = null; + ReactiveTransactionAttemptContext atr = null; if (resource != null) { atr = resource.getAttemptContextReactive(); } else { CouchbaseResourceHolder holder = (CouchbaseResourceHolder) TransactionSynchronizationManager .getResource(template.getCouchbaseClientFactory().getCluster()); if (holder != null && holder.getSession() != null) { - atr = holder.getSession().getAttemptContextReactive(); + atr = holder.getSession().getReactiveTransactionAttemptContext(); } } return atr; diff --git a/src/main/java/com/example/demo/SpringTransactionGetResult.java b/src/main/java/com/example/demo/SpringTransactionGetResult.java index 365751fc9..6e02d4d98 100644 --- a/src/main/java/com/example/demo/SpringTransactionGetResult.java +++ b/src/main/java/com/example/demo/SpringTransactionGetResult.java @@ -1,6 +1,6 @@ package com.example.demo; -import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.client.java.transactions.TransactionGetResult; public class SpringTransactionGetResult { diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index f4203bcf2..4136c24a1 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -18,9 +18,7 @@ import java.io.Closeable; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import org.springframework.dao.support.PersistenceExceptionTranslator; import com.couchbase.client.java.Bucket; @@ -79,8 +77,7 @@ public interface CouchbaseClientFactory extends Closeable { */ PersistenceExceptionTranslator getExceptionTranslator(); - ClientSession getSession(ClientSessionOptions options, Transactions transactions, - TransactionConfig config , AttemptContextReactive atr); + ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr); //CouchbaseClientFactory with(CouchbaseStuffHandle txOp); diff --git a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java index cfeab6cef..2d18a2013 100644 --- a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java @@ -20,9 +20,7 @@ import com.couchbase.client.java.ClusterInterface; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.couchbase.transaction.ClientSessionOptions; import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; @@ -88,7 +86,7 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* */ PersistenceExceptionTranslator getExceptionTranslator(); - Mono getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config); + Mono getSession(ClientSessionOptions options); String getBucketName(); @@ -96,10 +94,7 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* void close() throws IOException; - Mono getSession(ClientSessionOptions options); - - ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, - AttemptContextReactive atr); + ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext ctx); /* * (non-Javadoc) diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index 0080196a7..6c8243cc7 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -19,6 +19,10 @@ import java.time.temporal.ChronoUnit; import java.util.function.Supplier; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; import org.springframework.data.couchbase.transaction.ClientSession; @@ -34,11 +38,7 @@ import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; import com.couchbase.client.java.env.ClusterEnvironment; -import com.couchbase.transactions.AttemptContext; -import com.couchbase.transactions.AttemptContextReactive; import com.couchbase.transactions.AttemptContextReactiveAccessor; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; /** * The default implementation of a {@link CouchbaseClientFactory}. @@ -60,8 +60,9 @@ public SimpleCouchbaseClientFactory(final String connectionString, final Authent public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, final String bucketName, final String scopeName) { - this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator))), - bucketName, scopeName); + this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator) + // todo gp disabling cleanupLostAttempts to simplify output during development + .environment(env -> env.transactionsConfig(TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)))))), bucketName, scopeName); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, @@ -127,14 +128,15 @@ public PersistenceExceptionTranslator getExceptionTranslator() { } @Override - public ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, - AttemptContextReactive atr) { - // can't we just use AttemptContextReactive everywhere? Instead of creating AttemptContext(atr), then + public ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr) { + // todo gp needed? + return null; + // can't we just use ReactiveTransactionAttemptContext everywhere? Instead of creating TransactionAttemptContext(atr), then // accessing at.getACR() ? - AttemptContext at = AttemptContextReactiveAccessor - .from(atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive())); - - return new ClientSessionImpl(this, transactions, config, at); +// TransactionAttemptContext at = AttemptContextReactiveAccessor +// .from(atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive())); +// +// return new ClientSessionImpl(this, at); } // @Override @@ -149,9 +151,10 @@ public ClientSession getSession(ClientSessionOptions options, Transactions trans @Override public void close() { - if (cluster instanceof OwnedSupplier) { - cluster.get().disconnect(); - } + // todo gp +// if (cluster instanceof OwnedSupplier) { +// cluster.get().disconnect(); +// } } private static Duration now() { diff --git a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java index 46159bf33..90dd31d88 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java @@ -4,6 +4,7 @@ import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; import com.couchbase.client.java.ClusterInterface; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.transactions.AttemptContextReactiveAccessor; import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Mono; @@ -24,9 +25,6 @@ import com.couchbase.client.java.Cluster; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; public class SimpleReactiveCouchbaseClientFactory implements ReactiveCouchbaseClientFactory { final Mono cluster; @@ -108,13 +106,6 @@ public PersistenceExceptionTranslator getExceptionTranslator() { return exceptionTranslator; } - @Override - public Mono getSession(ClientSessionOptions options, Transactions transactions, - TransactionConfig config /*, AttemptContextReactive atr*/) { - throw new RuntimeException("TODO: maybe not used"); - // return Mono.just(new ClientSessionImpl(this, transactions, config)); - } - @Override public void close() { cluster.block().disconnect(); @@ -122,16 +113,16 @@ public void close() { @Override public Mono getSession(ClientSessionOptions options) { // hopefully this gets filled in later - return Mono.from(Mono.just(new ClientSessionImpl(this, null, null, null))); // .startSession(options)); + return Mono.from(Mono.just(new ClientSessionImpl(this, null))); // .startSession(options)); } @Override - public ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, - AttemptContextReactive atr) { - - AttemptContextReactive at = atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive()); - - return new ClientSessionImpl(this, transactions, config, at); + public ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr) { + // todo gp needed? + return null; +// ReactiveTransactionAttemptContext at = atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive()); +// +// return new ClientSessionImpl(this, at); } @Override @@ -234,12 +225,6 @@ public PersistenceExceptionTranslator getExceptionTranslator() { return delegate.getExceptionTranslator(); } - @Override - public Mono getSession(ClientSessionOptions options, Transactions transactions, - TransactionConfig config) { - return Mono.just(session); - } - @Override public String getBucketName() { return delegate.getBucketName(); @@ -261,13 +246,12 @@ public void close() throws IOException { */ @Override public Mono getSession(ClientSessionOptions options) { - return getSession(options, null, null); + return Mono.just(getSession(options, null)); } @Override - public ClientSession getSession(ClientSessionOptions options, Transactions transactions, TransactionConfig config, - AttemptContextReactive atr) { - return delegate.getSession(options, transactions, config, atr); + public ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr) { + return delegate.getSession(options, atr); } /* diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 127044c1d..efa98996e 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -46,7 +46,7 @@ import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; -import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; @@ -67,10 +67,6 @@ import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.json.JacksonTransformers; import com.couchbase.client.java.json.JsonValueModule; -import com.couchbase.transactions.TransactionDurabilityLevel; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -334,41 +330,46 @@ public ObjectMapper couchbaseObjectMapper() { return mapper; } - @Bean(COUCHBASE_TRANSACTIONS) - public Transactions getTransactions(Cluster cluster, TransactionConfig transactionConfig) { - return Transactions.create(cluster, transactionConfig); - } - - @Bean - public TransactionConfig transactionConfig() { - return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofSeconds(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); - } + // todo gp how to DI this into the Cluster creation esp. as it creates a CoreTransactionConfig +// @Bean +// public TransactionsConfig transactionConfig() { +// return TransactionsConfig.builder().build(); +// } @Bean(BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) ReactiveCouchbaseTransactionManager reactiveTransactionManager( - ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, Transactions transactions) { - return new ReactiveCouchbaseTransactionManager(reactiveCouchbaseClientFactory, transactions); + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) { + return new ReactiveCouchbaseTransactionManager(reactiveCouchbaseClientFactory); } +// @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) +// CouchbaseTransactionManager transactionManager(CouchbaseClientFactory couchbaseClientFactory) { +// return new CouchbaseTransactionManager(couchbaseClientFactory); +// } + + // todo gp experimenting with making CouchbaseSimpleCallbackTransactionManager the default - but it doesn't play + // nice with MR's changes to insert CouchbaseTransactionInterceptor @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) - CouchbaseTransactionManager transactionManager(CouchbaseClientFactory couchbaseClientFactory, - Transactions transactions) { - return new CouchbaseTransactionManager(couchbaseClientFactory, transactions); + CouchbaseSimpleCallbackTransactionManager transactionManager(CouchbaseClientFactory clientFactory) { + return new CouchbaseSimpleCallbackTransactionManager(clientFactory); } +// @Bean(BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) +// CouchbaseSimpleCallbackTransactionManager simpleCallbackTransactionManager(CouchbaseClientFactory clientFactory) { +// return new CouchbaseSimpleCallbackTransactionManager(clientFactory); +// } + + /** * Blocking Transaction Manager * * @param couchbaseTemplate - * @param transactionConfig * @return */ - @Bean(BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) - CouchbaseCallbackTransactionManager callbackTransactionManager(CouchbaseTemplate couchbaseTemplate, ReactiveCouchbaseTemplate couchbaseReactiveTemplate, - TransactionConfig transactionConfig) { - return new CouchbaseCallbackTransactionManager(couchbaseTemplate, couchbaseReactiveTemplate, transactionConfig); - } +// @Bean(BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) +// CouchbaseCallbackTransactionManager callbackTransactionManager(CouchbaseTemplate couchbaseTemplate, ReactiveCouchbaseTemplate couchbaseReactiveTemplate) { +// return new CouchbaseCallbackTransactionManager(couchbaseTemplate, couchbaseReactiveTemplate); +// } /** * Configure whether to automatically create indices for domain types by deriving the from the entity or not. diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index f462c0480..100c841e5 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -68,5 +68,5 @@ public class BeanNames { public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager"; - public static final String COUCHBASE_CALLBACK_TRANSACTION_MANAGER = "couchbaseCallbackTransactionManager"; + public static final String COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER = "couchbaseSimpleCallbackTransactionManager"; } diff --git a/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java b/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java new file mode 100644 index 000000000..9899a99f6 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java @@ -0,0 +1,61 @@ +package org.springframework.data.couchbase.core; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.lang.Nullable; +import reactor.core.publisher.Mono; + +import java.util.Optional; +import java.util.function.Function; + +// todo gp better name +@Stability.Internal +class GenericSupportHelper { + public final CouchbaseDocument converted; + public final Collection collection; + public final @Nullable TransactionAttemptContext ctx; + + public GenericSupportHelper(CouchbaseDocument doc, Collection collection, @Nullable TransactionAttemptContext ctx) { + this.converted = doc; + this.collection = collection; + this.ctx = ctx; + } +} + +// todo gp better name +@Stability.Internal +public class GenericSupport { + public static Mono one(ReactiveCouchbaseTemplate template, + String scopeName, + String collectionName, + ReactiveTemplateSupport support, + T object, + Function> nonTransactional, + Function> transactional) { + // todo gp how safe is this? I think we can switch threads potentially + Optional ctxr = Optional.ofNullable((TransactionAttemptContext) + org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); + + return template.getCouchbaseClientFactory().withScope(scopeName).getCollection(collectionName) + .flatMap(collection -> + support.encodeEntity(object) + .flatMap(converted -> { + GenericSupportHelper gsh = new GenericSupportHelper(converted, collection, ctxr.orElse(null)); + if (!ctxr.isPresent()) { + return nonTransactional.apply(gsh); + } else { + return transactional.apply(gsh); + } + })) + .onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 89ad24ea6..36083b53d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -17,12 +17,9 @@ import static com.couchbase.client.java.kv.GetAndTouchOptions.getAndTouchOptions; -import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.TransactionQueryOptions; -import com.example.demo.CouchbaseTransactionalTemplate; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.ClientSession; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,6 +27,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,16 +89,43 @@ public Mono one(final String id) { CommonOptions gOptions = initGetOptions(); PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, txCtx, domainType); LOG.trace("findById {}", pArgs); - ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).block().reactive(); - Mono tmpl = template.doGetTemplate(); - //AttemptContextReactive ctx = CouchbaseTransactionalTemplate.getContextReactive(template); +// return GenericSupport.one(template, scope, collection, support, object, +// (GenericSupportHelper support) -> { +// if (pArgs.getOptions() instanceof GetAndTouchOptions) { +// return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()).flatMap( +// result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); +// } else { +// return rc.get(id, (GetOptions) pArgs.getOptions()).flatMap( +// result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); +// } +// }, +// (GenericSupportHelper support) -> { +// return s.getReactiveTransactionAttemptContext().get(rc, id) +// // todo gp no cas +// .flatMap(result -> support.decodeEntity(id, result.contentAsObject().toString(), 0, +// domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result), s)); +// } +// })).onErrorResume(throwable -> { +// if (throwable instanceof DocumentNotFoundException) { +// return Mono.empty(); +// } +// return Mono.error(throwable); +// }); + + Optional ctxr = Optional.ofNullable((TransactionAttemptContext) + org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); + + com.couchbase.client.java.Collection coll = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).block(); + ReactiveCollection rc = coll.reactive(); + +// Mono tmpl = template.doGetTemplate(); + //ReactiveTransactionAttemptContext ctx = CouchbaseTransactionalTemplate.getContextReactive(template); //ClientSession session = CouchbaseTransactionalTemplate.getSession(template); - Mono reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) - .flatMap(s -> { - if ( s == null || s.getAttemptContextReactive() == null ) { + Mono reactiveEntity = Mono.defer(() -> { + if (!ctxr.isPresent()) { if (pArgs.getOptions() instanceof GetAndTouchOptions) { return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()).flatMap( result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); @@ -109,11 +134,18 @@ public Mono one(final String id) { result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); } } else { - return s.getAttemptContextReactive().get(rc, id) - .flatMap(result -> support.decodeEntity(id, result.contentAsObject().toString(), result.cas(), - domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result), s)); + return Mono.defer(() -> { + TransactionGetResult result = ctxr.get().get(coll, id); + // todo gp no cas + return support.decodeEntity(id, result.contentAsObject().toString(), 0, + domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result), null) + .doOnNext(out -> { + // todo gp is this safe? are we on the right thread? + // org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(out, result); + }); + }); } - })); + }); return reactiveEntity.onErrorResume(throwable -> { if (throwable instanceof DocumentNotFoundException) { @@ -135,6 +167,7 @@ private TransactionGetOptions buildTranasactionOptions(ReplaceOptions buildOptio } */ + @Override public Flux all(final Collection ids) { return Flux.fromIterable(ids).flatMap(this::one); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 5c53a8a4c..6bc8dc1b0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +import com.couchbase.client.java.transactions.TransactionQueryResult; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; @@ -22,7 +24,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; @@ -34,7 +35,6 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; -import com.couchbase.transactions.TransactionQueryOptions; /** * {@link ReactiveFindByQueryOperation} implementations for Couchbase. @@ -207,14 +207,16 @@ public Flux all() { //if (pArgs.getTxOp() == null && txOp == null) { // too early to find TxOp - transactional() has not yet been called allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) .flatMap(s -> { - if ( s == null || s.getAttemptContextReactive() == null ) { + if ( s == null || s.getReactiveTransactionAttemptContext() == null ) { QueryOptions opts = buildOptions(pArgs.getOptions()); return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return s.getAttemptContextReactive() + Mono tqr = s.getReactiveTransactionAttemptContext() .query(statement, opts); + // todo gp do something with tqr + return Mono.empty(); } })); Mono finalAllResult = allResult; @@ -279,14 +281,16 @@ public Mono count() { */ countResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) .flatMap(s -> { - if ( s == null || s.getAttemptContextReactive() == null ) { + if ( s == null || s.getReactiveTransactionAttemptContext() == null ) { QueryOptions opts = buildOptions(pArgs.getOptions()); return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return s.getAttemptContextReactive() + Mono tqr = s.getReactiveTransactionAttemptContext() .query(statement, opts); + // todo gp do something with tqr + return Mono.empty(); } })); Mono finalCountResult = countResult; diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 8622f75e9..16c57b629 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -15,13 +15,8 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.java.Cluster; -import com.example.demo.CouchbaseTransactionalTemplate; -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import com.couchbase.client.java.transactions.TransactionGetResult; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.transaction.reactive.TransactionContext; -import org.springframework.transaction.reactive.TransactionContextManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; @@ -39,14 +33,9 @@ import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; -import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.TransactionInsertOptions; - -import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; -import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { @@ -117,68 +106,24 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { @Override public Mono one(T object) { - // ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager - // .getResource(getRequiredDatabaseFactory()); - - // ((ReactiveCouchbaseResourceHolder) - // TransactionSynchronizationManager.forCurrentTransaction().flatMap((synchronizationManager) -> { - // return Mono.just(synchronizationManager.getResource( template.getCouchbaseClientFactory())); - // }).block()).getSession().getAttemptContextReactive() / - // if (TransactionSynchronizationManager.hasResource(template.getCouchbaseClientFactory())){ - // - // } - // the template should have the session(???) PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); LOG.trace("insertById {}", pArgs); - Mono tmpl = template.doGetTemplate(); - //ClientSession session = CouchbaseTransactionalTemplate.getSession(template); - Mono reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { - if (s == null || s.getAttemptContextReactive() == null) { - return template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()) - .flatMap(collection -> collection.reactive() - .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - .flatMap( - result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); - } else { - return s.getAttemptContextReactive() - .insert( - tp.doGetDatabase().block().bucket(tp.getBucketName()).reactive() - .scope(pArgs.getScope() != null ? pArgs.getScope() : DEFAULT_SCOPE) - .collection(pArgs.getCollection() != null ? pArgs.getCollection() : DEFAULT_COLLECTION), - converted.getId(), converted.getContent(), buildTxOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), new TransactionResultHolder(result), s)); - } - }))); - // .flatMap(converted ->/* rc */tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getCluster().flatMap( cl -> - // cl.bucket("my_bucket").reactive() - // .defaultCollection() - // .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - // .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))))); - /* - } else { - reactiveEntity = support.encodeEntity(object).flatMap(converted -> pArgs.getTxOp().getAttemptContextReactive() // transactional() - // needs - // to - // have - // initted - // acr - .insert(template.doGetDatabase().block().bucket("my_bucket").reactive().defaultCollection(), - converted.getId(), converted.getContent(), buildTxOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), - pArgs.getTxOp().transactionResultHolder(result)))); - } - */ - - return reactiveEntity.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + return GenericSupport.one(template, scope, collection, support, object, + (GenericSupportHelper support) -> { + return support.collection.reactive().insert(support.converted.getId(), support.converted.export(), buildOptions(pArgs.getOptions(), support.converted)) + .flatMap(result -> + this.support.applyResult(object, support.converted, support.converted.getId(), result.cas(), null)); + }, + (GenericSupportHelper support) -> { + return template.doGetTemplate() + // todo gp this runnable probably not great + .flatMap(tp -> Mono.defer(() -> { + TransactionGetResult result = support.ctx.insert(support.collection, support.converted.getId(), support.converted.getContent()); + // todo gp don't have result.cas() anymore - needed? + return this.support.applyResult(object, support.converted, support.converted.getId(), 0L, new TransactionResultHolder(result), null); + })); + }); } @Override @@ -190,10 +135,6 @@ public InsertOptions buildOptions(InsertOptions options, CouchbaseDocument doc) return OptionsBuilder.buildInsertOptions(options, persistTo, replicateTo, durabilityLevel, expiry, doc); } - private TransactionInsertOptions buildTxOptions(InsertOptions buildOptions, CouchbaseDocument doc) { - return OptionsBuilder.buildTxInsertOptions(buildOptions(buildOptions, doc)); - } - @Override public TerminatingInsertById withOptions(final InsertOptions options) { Assert.notNull(options, "Options must not be null."); @@ -220,6 +161,7 @@ public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel durabilityLevel, expiry, txCtx, support); } + // todo gp need to figure out how to handle options re transactions. E.g. many non-transactional insert options, like this, aren't supported @Override public InsertByIdInScope withDurability(final PersistTo persistTo, final ReplicateTo replicateTo) { Assert.notNull(persistTo, "PersistTo must not be null."); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 815679dee..8bc0b70a0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -34,8 +34,6 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.TransactionGetResult; -import com.couchbase.transactions.components.TransactionLinks; public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { @@ -71,10 +69,6 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final Long cas; private final CouchbaseStuffHandle txCtx; - private final TransactionLinks tl = new TransactionLinks(Optional.empty(), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty(), false, Optional.empty(), Optional.empty()); - ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel, Long cas, CouchbaseStuffHandle txCtx) { @@ -101,9 +95,13 @@ public Mono one(final String id) { removeResult = rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); } else { Transcoder transcoder = template.getCouchbaseClientFactory().getCluster().block().environment().transcoder(); - TransactionGetResult doc = new TransactionGetResult(id, null, 0, rc, tl, null, Optional.empty(), transcoder, - null); - removeResult = pArgs.getTxOp().getAttemptContextReactive().remove(doc).map(r -> new RemoveResult(id, 0, null)); + // todo gp we definitely don't want to be creating TransactionGetResult. It's essential that this is passed + // from a previous ctx.get(). So we know if this doc is in a transaction and can safely detect + // write-write conflicts. This will be a blocker. + // Looks like replace is solving this with a getTransactionHolder? +// TransactionGetResult doc = new TransactionGetResult(id, null, 0, rc, tl, null, Optional.empty(), transcoder, +// null); + removeResult = pArgs.getTxOp().getAttemptContextReactive().remove(null).map(r -> new RemoveResult(id, 0, null)); } return removeResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index 2df3a5aef..1e3295e9c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +import com.couchbase.client.java.transactions.TransactionQueryResult; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import reactor.core.publisher.Flux; @@ -34,7 +36,6 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; -import com.couchbase.transactions.TransactionQueryOptions; public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQueryOperation { @@ -90,7 +91,8 @@ public Flux all() { : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(buildQueryOptions(pArgs.getOptions())); - allResult = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rs, statement, opts); + Mono tqr = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rs, statement, opts); + // todo gp do something with tqr } Mono finalAllResult = allResult; return Flux.defer(() -> finalAllResult.onErrorMap(throwable -> { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 242cb3f9d..1afd1849f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -15,9 +15,14 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.Method; import java.time.Duration; import java.util.Collection; import java.util.Optional; @@ -36,7 +41,6 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; -import com.couchbase.transactions.components.TransactionLinks; public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { @@ -68,10 +72,6 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final CouchbaseStuffHandle txCtx; private final ReactiveTemplateSupport support; - private final TransactionLinks tl = new TransactionLinks(Optional.empty(), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), - Optional.empty(), Optional.empty(), false, Optional.empty(), Optional.empty()); - ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseStuffHandle txCtx, @@ -122,36 +122,90 @@ public Mono one(T object) { public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("replaceById {}", pArgs); - Mono tmpl = template.doGetTemplate(); - Mono reactiveEntity; - - CouchbaseDocument converted = support.encodeEntity(object).block(); - reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { - if (s == null || s.getAttemptContextReactive() == null) { - System.err.println("ReactiveReplaceById: not"); - Mono op = template.getCouchbaseClientFactory() - .withScope(pArgs.getScope()).getCollection(pArgs.getCollection()); - return op.flatMap(collection -> collection.reactive() - .replace(converted.getId(), converted.export(), - buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); - } else { - System.err.println("ReactiveReplaceById: transaction"); - return s.getAttemptContextReactive() - .replace(s.transactionResultHolder(getTransactionHolder(object)).transactionGetResult(), - converted.getContent()) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), - new TransactionResultHolder(result), s)); - } - })); - return reactiveEntity.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + return GenericSupport.one(template, scope, collection, support, object, + (GenericSupportHelper support) -> { + CouchbaseDocument converted = support.converted; + + return support.collection.reactive() + .replace(converted.getId(), converted.export(), + buildReplaceOptions(pArgs.getOptions(), object, converted)) + .flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), null)); + }, + (GenericSupportHelper support) -> { + CouchbaseDocument converted = support.converted; + + // todo gp replace is a nightmare... + // Where to put and how to pass the TransactionGetResult + // - Idea: TransactionSynchronizationManager.bindResource + // - Idea: use @Version as an index into Map + // - As below, one idea is not to store it at all. + // Person could have been fetched outside of @Transactional block. Need to flat out prevent. Right?? + // - Maybe not. Could have the replaceById do a ctx.get(), and check the CAS matches the Person (will mandate @Version on Person). + // - Could always do that in fact. Then no need to hold onto TransactionGetResult anywhere - but slower too (could optimise later). + // - And if had get-less replaces, could pass in the CAS. + // - Note: if Person was fetched outside the transaction, the transaction will inevitably expire (continuous CAS mismatch). + // -- Will have to doc that the user generally wants to do the read inside the txn. + // -- Can we detect this scenario and reject at runtime? That would also probably need storing something in Person. + +// TransactionGetResult gr = (TransactionGetResult) org.springframework.transaction.support.TransactionSynchronizationManager.getResource(object); + TransactionGetResult gr = support.ctx.get(support.collection, converted.getId()); + + // todo gp if we need this of course needs to be exposed nicely + CoreTransactionGetResult internal; + try { + Method method = TransactionGetResult.class.getDeclaredMethod("internal"); + method.setAccessible(true); + internal = (CoreTransactionGetResult) method.invoke(gr); + } + catch (Throwable err) { + throw new RuntimeException(err); + } + + if (internal.cas() != support.converted.version) { + // todo gp really want to set internal state and raise a TransactionOperationFailed + throw new RetryTransactionException(); + } + + support.ctx.replace(gr, converted.getContent()); + // todo gp no CAS + return this.support.applyResult(object, converted, converted.getId(), 0L, null, null); + }); + +// Mono tmpl = template.doGetTemplate(); +// Mono reactiveEntity; +// +// Optional ctxr = Optional.ofNullable((TransactionAttemptContext) +// org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); +// +// CouchbaseDocument converted = support.encodeEntity(object).block(); +// reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { +// if (s == null || s.getReactiveTransactionAttemptContext() == null) { +// System.err.println("ReactiveReplaceById: not"); +// Mono op = template.getCouchbaseClientFactory() +// .withScope(pArgs.getScope()).getCollection(pArgs.getCollection()); +// return op.flatMap(collection -> collection.reactive() +// .replace(converted.getId(), converted.export(), +// buildReplaceOptions(pArgs.getOptions(), object, converted)) +// .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); +// } else { +// System.err.println("ReactiveReplaceById: transaction"); +// return s.getReactiveTransactionAttemptContext() +// .replace(s.transactionResultHolder(getTransactionHolder(object)).transactionGetResult(), +// converted.getContent()) +// // todo gp no CAS +// .flatMap(result -> support.applyResult(object, converted, converted.getId(), 0L, +// new TransactionResultHolder(result), s)); +// } +// })); +// +// return reactiveEntity.onErrorMap(throwable -> { +// if (throwable instanceof RuntimeException) { +// return template.potentiallyConvertRuntimeException((RuntimeException) throwable); +// } else { +// return throwable; +// } +// }); } private Integer getTransactionHolder(T object) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index fc112537e..6dadac473 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -85,7 +85,7 @@ public Mono one(T object) { Mono tmpl = template.doGetTemplate(); Mono reactiveEntity = support.encodeEntity(object) .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { - if (s == null || s.getAttemptContextReactive() == null) { + if (s == null || s.getReactiveTransactionAttemptContext() == null) { return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).flatMap(collection -> collection.reactive() .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java index ead8146ed..02566f767 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java @@ -56,6 +56,9 @@ public class CouchbaseDocument implements CouchbaseStorable { */ private int expiration; + // todo gp + public long version; + /** * Creates a completely empty {@link CouchbaseDocument}. */ diff --git a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java index e7ce86357..aacc2a4f0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Optional; +import com.couchbase.client.java.transactions.TransactionQueryOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -50,9 +51,6 @@ import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import com.couchbase.transactions.TransactionInsertOptions; -import com.couchbase.transactions.TransactionQueryOptions; -import com.couchbase.transactions.TransactionReplaceOptions; public class OptionsBuilder { @@ -134,13 +132,6 @@ public static InsertOptions buildInsertOptions(InsertOptions options, PersistTo return options; } - public static TransactionInsertOptions buildTxInsertOptions(InsertOptions options) { - options = options != null ? options : InsertOptions.insertOptions(); - InsertOptions.Built built = options.build(); - TransactionInsertOptions txOptions = TransactionInsertOptions.insertOptions(); - return txOptions; - } - public static UpsertOptions buildUpsertOptions(UpsertOptions options, PersistTo persistTo, ReplicateTo replicateTo, DurabilityLevel durabilityLevel, Duration expiry, CouchbaseDocument doc) { options = options != null ? options : UpsertOptions.upsertOptions(); @@ -182,18 +173,6 @@ public static ReplaceOptions buildReplaceOptions(ReplaceOptions options, Persist return options; } - public static Object buildTransactionReplaceOptions(ReplaceOptions options) { - ReplaceOptions.Built built = options.build(); - TransactionReplaceOptions txOptions = TransactionReplaceOptions.replaceOptions(); - return txOptions; - } - - public static TransactionReplaceOptions buildTransactionUpsertOptions(ReplaceOptions options) { - ReplaceOptions.Built built = options.build(); - TransactionReplaceOptions txOptions = TransactionReplaceOptions.replaceOptions(); - return txOptions; - } - public static RemoveOptions buildRemoveOptions(RemoveOptions options, PersistTo persistTo, ReplicateTo replicateTo, DurabilityLevel durabilityLevel, Long cas) { options = options != null ? options : RemoveOptions.removeOptions(); diff --git a/src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java b/src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java deleted file mode 100644 index 4b26edc4a..000000000 --- a/src/main/java/org/springframework/data/couchbase/core/transaction/SDCouchbaseTransactions.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2021 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.core.transaction; - -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.TransactionGetResult; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.TransactionsReactive; -import com.couchbase.transactions.config.PerTransactionConfigBuilder; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import reactor.core.publisher.Mono; - -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -public class SDCouchbaseTransactions { - - Transactions transactions; - Map getResultMap = new HashMap<>(); - private AttemptContextReactive ctx; - - public SDCouchbaseTransactions(Transactions transactions) { - this.transactions = transactions; - } - - public TransactionsReactive reactive(){ - return transactions.reactive(); - } - - public AttemptContextReactive getCtx(){ - return ctx; - } - - // public Mono reactive(Function> transactionLogic) { - // return reactive(transactionLogic, true); - // } - /** - * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default PerTransactionConfig. - */ - public Mono reactive(Function> transactionLogic/*, boolean commit*/) { - return transactions.reactive((ctx) -> { - setAttemptTransactionReactive(ctx); - return transactionLogic.apply(ctx); }, PerTransactionConfigBuilder.create().build()/*, commit*/); - - } - - public TransactionResultHolder transactionGetResult(Integer key){ - return getResultMap.get(key); - } - - public TransactionResultHolder transactionGetResult(TransactionGetResult result){ - TransactionResultHolder holder = new TransactionResultHolder(result); - getResultMap.put(System.identityHashCode(holder), holder); - return holder; - } - - public void setAttemptTransactionReactive(AttemptContextReactive ctx) { - this.ctx = ctx; - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java b/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java index 6e9be67da..d2236d520 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java +++ b/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java @@ -28,6 +28,7 @@ * * @author Michael Reiche */ +// todo gp can we give this a different name since there is an existing TransactionResult @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) @Documented diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java index c3282a489..55cc2dc3f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java @@ -16,8 +16,9 @@ package org.springframework.data.couchbase.repository.support; -import com.couchbase.transactions.SingleQueryTransactionResult; -import com.couchbase.transactions.TransactionGetResult; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.transactions.TransactionGetResult; +import reactor.util.annotation.Nullable; /** * Holds previously obtained Transaction*Result @@ -26,26 +27,29 @@ */ public class TransactionResultHolder { - TransactionGetResult getResult; - SingleQueryTransactionResult singleQueryResult; + private final @Nullable TransactionGetResult getResult; + // todo gp needed? + private final @Nullable QueryResult singleQueryResult; public TransactionResultHolder(TransactionGetResult getResult) { // we don't need the content and we don't have access to the transcoder an txnMeta (and we don't need them either). - this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), - getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); + // todo gp will need to expose a copy ctor if a copy is needed + this.getResult = getResult; +// this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), +// getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); this.singleQueryResult = null; } - public TransactionResultHolder(SingleQueryTransactionResult singleQueryResult) { + public TransactionResultHolder(QueryResult singleQueryResult) { this.getResult = null; this.singleQueryResult = singleQueryResult; } - public TransactionGetResult transactionGetResult() { + public @Nullable TransactionGetResult transactionGetResult() { return getResult; } - public SingleQueryTransactionResult singleQueryResult() { + public @Nullable QueryResult singleQueryResult() { return singleQueryResult(); } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java index f3f79ac63..ca968753a 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java @@ -3,24 +3,21 @@ import com.couchbase.client.java.AsyncCluster; import com.couchbase.client.java.Scope; -import com.couchbase.transactions.AttemptContext; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.TransactionGetResult; -import com.couchbase.transactions.TransactionQueryOptions; -import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.reactivestreams.Publisher; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import reactor.core.publisher.Mono; -import java.util.Map; - /** * ClientSession. There is only one implementation - ClientSessionImpl * The SpringTransaction framework relies on the client session to perform commit() and abort() - * and therefore it has an AttemptContextReactive + * and therefore it has a ReactiveTransactionAttemptContext * * @author Michael Reiche */ +// todo gp understand why this is needed public interface ClientSession /*extends com.mongodb.session.ClientSession*/ { Mono getScope(); @@ -33,9 +30,9 @@ public interface ClientSession /*extends com.mongodb.session.ClientSession*/ { void notifyOperationInitiated(Object var1); - //void setAttemptContextReactive(AttemptContextReactive atr); + //void setAttemptContextReactive(ReactiveTransactionAttemptContext atr); - AttemptContextReactive getAttemptContextReactive(); + ReactiveTransactionAttemptContext getReactiveTransactionAttemptContext(); TransactionOptions getTransactionOptions(); @@ -43,8 +40,6 @@ public interface ClientSession /*extends com.mongodb.session.ClientSession*/ { void startTransaction(); - void startTransaction(TransactionConfig var1); - Publisher commitTransaction(); Publisher abortTransaction(); @@ -61,7 +56,7 @@ public interface ClientSession /*extends com.mongodb.session.ClientSession*/ { TransactionResultHolder transactionResultHolder(Integer key); - AttemptContext getAttemptContext(); + TransactionAttemptContext getTransactionAttemptContext(); - //ClientSession with(AttemptContextReactive atr); + //ClientSession with(ReactiveTransactionAttemptContext atr); } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java index b9db58c1e..eaae9bf68 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java @@ -1,11 +1,14 @@ package org.springframework.data.couchbase.transaction; -import com.couchbase.transactions.AttemptContext; -import com.couchbase.transactions.AttemptContextReactiveAccessor; -import com.couchbase.transactions.error.external.TransactionOperationFailed; +import com.couchbase.client.core.transaction.support.AttemptState; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.Transactions; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; +import org.springframework.transaction.reactive.TransactionContext; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; @@ -27,14 +30,6 @@ import com.couchbase.client.java.AsyncCluster; import com.couchbase.client.java.Scope; import com.couchbase.client.java.env.ClusterEnvironment; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.TransactionContext; -import com.couchbase.transactions.TransactionGetResult; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.MergedTransactionConfig; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; -import com.couchbase.transactions.support.AttemptStates; public class ClientSessionImpl implements ClientSession { @@ -44,47 +39,45 @@ public class ClientSessionImpl implements ClientSession { Scope scope; boolean commitInProgress = false; boolean messageSentInCurrentTransaction = true; // needs to be true for commit - AttemptStates transactionState = AttemptStates.NOT_STARTED; + // todo gp probably should not be duplicating CoreTransactionAttemptContext state outside of it + AttemptState transactionState = AttemptState.NOT_STARTED; TransactionOptions transactionOptions; - Transactions transactions; TransactionContext ctx; - TransactionConfig config; - AttemptContextReactive atr = null; - AttemptContext at = null; + ReactiveTransactionAttemptContext atr = null; + TransactionAttemptContext at = null; Map getResultMap = new HashMap<>(); public ClientSessionImpl(){} - public ClientSessionImpl(ReactiveCouchbaseClientFactory couchbaseClientFactory, Transactions transactions, - TransactionConfig config, AttemptContextReactive atr) { - this.transactions = transactions; + public ClientSessionImpl(ReactiveCouchbaseClientFactory couchbaseClientFactory, ReactiveTransactionAttemptContext atr) { scopeRx = couchbaseClientFactory.getScope(); - this.config = config == null - ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() - : config; - MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); - ClusterEnvironment environment = couchbaseClientFactory.getCluster().block().environment(); - ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), - now(), Duration.ZERO, merged); - // does this not need an non-reactive AttemptContext? + // todo gp hopefully none of this is needed +// this.config = config == null +// ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() +// : config; +// MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); +// ClusterEnvironment environment = couchbaseClientFactory.getCluster().block().environment(); +// ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), +// now(), Duration.ZERO, merged); + // does this not need an non-reactive TransactionAttemptContext? this.atr = atr; } - public ClientSessionImpl(CouchbaseClientFactory couchbaseClientFactory, Transactions transactions, - TransactionConfig config, AttemptContext at) { - this.transactions = transactions; + public ClientSessionImpl(CouchbaseClientFactory couchbaseClientFactory, TransactionAttemptContext at) { + // todo gp hopefully none of this is needed +// this.transactions = transactions; scope = couchbaseClientFactory.getScope(); - this.config = config == null - ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() - : config; - MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); +// this.config = config == null +// ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() +// : config; +// MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); ClusterEnvironment environment = couchbaseClientFactory.getCluster().environment(); - ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), - now(), Duration.ZERO, merged); +// ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), +// now(), Duration.ZERO, merged); this.at = at; - if(at != null){ - this.atr = AttemptContextReactiveAccessor.getACR(at); - } +// if(at != null){ +// this.atr = AttemptContextReactiveAccessor.getACR(at); +// } } @Override @@ -108,24 +101,24 @@ public void notifyOperationInitiated(Object var1) { } //@Override - //public void setAttemptContextReactive(AttemptContextReactive atr){ + //public void setAttemptContextReactive(ReactiveTransactionAttemptContext atr){ // this.atr = atr; //} @Override - public AttemptContextReactive getAttemptContextReactive(){ + public ReactiveTransactionAttemptContext getReactiveTransactionAttemptContext(){ return atr; } @Override - public AttemptContext getAttemptContext(){ + public TransactionAttemptContext getTransactionAttemptContext(){ return at; } // setter that returns `this` //@Override - //public ClientSession with(AttemptContextReactive atr){ + //public ClientSession with(ReactiveTransactionAttemptContext atr){ // setAttemptContextReactive(atr); // return this; //} @@ -140,38 +133,35 @@ public AsyncCluster getWrapped() { return null; } + // todo gp @Override public void startTransaction() { - transactionState = AttemptStates.PENDING; - } - - @Override - public void startTransaction(TransactionConfig var1) { - startTransaction(); + transactionState = AttemptState.PENDING; } + // todo gp @Override public Publisher commitTransaction() { - if (this.transactionState == AttemptStates.ABORTED) { + if (this.transactionState == AttemptState.ABORTED) { throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction"); - } else if (this.transactionState == AttemptStates.NOT_STARTED) { + } else if (this.transactionState == AttemptState.NOT_STARTED) { throw new IllegalStateException("There is no transaction started"); } else if (!this.messageSentInCurrentTransaction) { // seems there should have been a messageSent. We just do nothing(?) - this.cleanupTransaction(AttemptStates.COMMITTED); + this.cleanupTransaction(AttemptState.COMMITTED); return Mono.create(MonoSink::success); } else { /*ReadConcern readConcern = this.transactionOptions.getReadConcern(); */ if (0 == 1/* readConcern == null*/) { throw new CouchbaseException("Invariant violated. Transaction options read concern can not be null"); } else { - boolean alreadyCommitted = this.commitInProgress || this.transactionState == AttemptStates.COMMITTED; + boolean alreadyCommitted = this.commitInProgress || this.transactionState == AttemptState.COMMITTED; this.commitInProgress = true; // this will fail with ctx.serialized() being Optional.empty() // how does the commit happen in transactions.reactive().run() ? /* return transactions.reactive().commit(ctx.serialized().get(), null).then().doOnSuccess(x -> { commitInProgress = false; - this.transactionState = AttemptStates.COMMITTED; + this.transactionState = AttemptState.COMMITTED; }).doOnError(CouchbaseException.class, this::clearTransactionContextOnError); */ // TODO MSR @@ -180,7 +170,7 @@ public Publisher commitTransaction() { /* return this.executor.execute((new CommitTransactionOperation(this.transactionOptions.getWriteConcern(), alreadyCommitted)).recoveryToken(this.getRecoveryToken()).maxCommitTime(this.transactionOptions.getMaxCommitTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS), readConcern, this).doOnTerminate(() -> { this.commitInProgress = false; - this.transactionState = AttemptStates.COMMITTED; + this.transactionState = AttemptState.COMMITTED; }).doOnError(CouchbaseException.class, this::clearTransactionContextOnError); */ @@ -189,7 +179,7 @@ public Publisher commitTransaction() { } } - public Mono executeImplicitCommit(AttemptContextReactive ctx) { + public Mono executeImplicitCommit(ReactiveTransactionAttemptContext ctx) { return Mono.defer(() -> { if (logger.isDebugEnabled()) { logger.debug(String.format("About to commit ctx %s", ctx)); @@ -201,19 +191,21 @@ public Mono executeImplicitCommit(AttemptContextReactive } else { //System.err.println(ctx.attemptId()+ " doing implicit commit"); // ctx.LOGGER.trace(); System.err.println("doing implicit commit"); - if(ctx != null) { - return ctx.commit() - .then(Mono.just(ctx)) - .onErrorResume(err -> Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, - ctx))); - } else { - at.commit(); - return Mono.empty(); - } + // todo gp ctx.commit() has gone in the SDK integration. Do we need this logic though? + return Mono.empty(); +// if(ctx != null) { +// return ctx.commit() +// .then(Mono.just(ctx)) +// .onErrorResume(err -> Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, +// ctx))); +// } else { +// at.commit(); +// return Mono.empty(); +// } } } else { System.err.println("Transaction already done"); - System.err.println(ctx.attemptId()+" Transaction already done"); // // ctx.LOGGER.trace(); + //System.err.println(ctx.attemptId()+" Transaction already done"); // // ctx.LOGGER.trace(); return Mono.just(ctx); } }); @@ -224,24 +216,26 @@ public Mono executeImplicitCommit(AttemptContextReactive @Override public Publisher abortTransaction() { System.err.println("**** abortTransaction ****"); - Assert.notNull(transactions, "transactions"); +// Assert.notNull(transactions, "transactions"); Assert.notNull(ctx, "ctx"); - Assert.notNull(ctx.serialized(), "ctx.serialized()"); - if (ctx.serialized().isPresent()) { - Assert.notNull(ctx.serialized().get(), "ctx.serialized().get()"); - return transactions.reactive().rollback(ctx.serialized().get(), null).then(); - } else { +// Assert.notNull(ctx.serialized(), "ctx.serialized()"); +// if (ctx.serialized().isPresent()) { +// Assert.notNull(ctx.serialized().get(), "ctx.serialized().get()"); +// return transactions.reactive().rollback(ctx.serialized().get(), null).then(); +// } else { return executeExplicitRollback(atr).then(); - } +// } } - private Mono executeExplicitRollback(AttemptContextReactive atr) { - if(at != null){ - at.rollback(); - return Mono.empty(); - } else { - return atr.rollback(); - } + private Mono executeExplicitRollback(ReactiveTransactionAttemptContext atr) { + // todo gp ctx.rollback() is removed + return Mono.empty(); +// if(at != null){ +// at.rollback(); +// return Mono.empty(); +// } else { +// return atr.rollback(); +// } } @Override @@ -264,7 +258,7 @@ public Object isCausallyConsistent() { return null; } - private void cleanupTransaction(AttemptStates attempState) {} + private void cleanupTransaction(AttemptState attempState) {} private void clearTransactionContext() {} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java index b145f0715..c5aa3417a 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java @@ -2,12 +2,11 @@ import java.util.Objects; +import com.couchbase.client.java.transactions.TransactionQueryOptions; import org.springframework.data.annotation.Immutable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import com.couchbase.transactions.TransactionQueryOptions; - @Immutable public final class ClientSessionOptions { private final Boolean causallyConsistent; diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java index 9f5b3f7f5..ee9facf31 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java @@ -25,11 +25,11 @@ import org.springframework.transaction.reactive.TransactionalOperator; import com.couchbase.client.core.error.CouchbaseException; -// import com.couchbase.transactions.AttemptContextReactive; +// import com.couchbase.transactions.ReactiveTransactionAttemptContext; /** - * This is a proxy for AttemptContextReactive that also has the transactionalOperator, so that it can provide the + * This is a proxy for ReactiveTransactionAttemptContext that also has the transactionalOperator, so that it can provide the * transactionalOperator to the repository and templates used within the transaction lambda via ctx.template(templ) and * ctx.repository(repo) */ @@ -39,7 +39,7 @@ public interface CouchbaseAttemptContextReactive { ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template); - static CouchbaseAttemptContextReactive proxyFor(/*AttemptContextReactive acr,*/ TransactionalOperator txOperator) { + static CouchbaseAttemptContextReactive proxyFor(/*ReactiveTransactionAttemptContext acr,*/ TransactionalOperator txOperator) { Class[] interfaces = new Class[] { /* AttemptContextReactiveInterface.class, */ CouchbaseAttemptContextReactive.class }; CouchbaseAttemptContextReactive proxyInstance = (CouchbaseAttemptContextReactive) Proxy.newProxyInstance( @@ -50,10 +50,10 @@ static CouchbaseAttemptContextReactive proxyFor(/*AttemptContextReactive acr,*/ class ACRInvocationHandler implements InvocationHandler { - // final AttemptContextReactive acr; + // final ReactiveTransactionAttemptContext acr; final TransactionalOperator txOperator; - public ACRInvocationHandler(/*AttemptContextReactive acr,*/ TransactionalOperator txOperator) { + public ACRInvocationHandler(/*ReactiveTransactionAttemptContext acr,*/ TransactionalOperator txOperator) { // this.acr = acr; this.txOperator = txOperator; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java index 27bbd7c94..e1904055b 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java @@ -1,294 +1,295 @@ -/* - * Copyright 2021 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.transaction; - -import reactor.core.publisher.Mono; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.concurrent.atomic.AtomicReference; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.reactive.TransactionContextManager; -import org.springframework.transaction.reactive.TransactionSynchronizationManager; -import org.springframework.transaction.support.AbstractPlatformTransactionManager; -import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; -import org.springframework.transaction.support.DefaultTransactionStatus; -import org.springframework.transaction.support.ResourceTransactionManager; -import org.springframework.transaction.support.SmartTransactionObject; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionSynchronizationUtils; -import org.springframework.util.Assert; - -import com.couchbase.client.java.Cluster; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.AttemptContextReactiveAccessor; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.error.external.TransactionOperationFailed; -import reactor.util.context.ContextView; - -/** - * Blocking TransactionManager - * - * @author Michael Nitschinger - * @author Michael Reiche - */ - -public class CouchbaseCallbackTransactionManager extends AbstractPlatformTransactionManager - implements DisposableBean, ResourceTransactionManager, CallbackPreferringPlatformTransactionManager { - - private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); - - private final CouchbaseTemplate template; - private final ReactiveCouchbaseTemplate reactiveTemplate; - private Transactions transactions; - private final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; - private final CouchbaseClientFactory couchbaseClientFactory; - - private ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction; - - public CouchbaseCallbackTransactionManager(CouchbaseTemplate template, ReactiveCouchbaseTemplate reactiveTemplate, - TransactionConfig transactionConfig) { - this.template = template; - this.reactiveTemplate = reactiveTemplate; - this.transactions = Transactions.create((Cluster) (template().getCouchbaseClientFactory().getCluster().block()), - transactionConfig); - this.reactiveCouchbaseClientFactory = this.reactiveTemplate.getCouchbaseClientFactory(); - this.couchbaseClientFactory = this.template.getCouchbaseClientFactory(); - } - - public ReactiveCouchbaseTemplate template() { - return reactiveTemplate; - } - - private CouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options, - AttemptContextReactive atr) { - - CouchbaseClientFactory databaseFactory = template.getCouchbaseClientFactory(); - - CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder( - databaseFactory.getSession(options, transactions, null, atr), databaseFactory); - return resourceHolder; - } - - @Override - public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { - final AtomicReference execResult = new AtomicReference<>(); - AtomicReference startTime = new AtomicReference<>(0L); - - Mono txnResult = transactions.reactive().run(ctx -> { - /* begin spring-data-couchbase transaction 1/2 */ - ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); - ReactiveCouchbaseResourceHolder reactiveResourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, - reactiveCouchbaseClientFactory); - - CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(clientSession, - template.getCouchbaseClientFactory()); - - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new) - . flatMap(synchronizationManager -> { - System.err.println("CallbackTransactionManager: " + this); - System.err.println("bindResource: " + reactiveCouchbaseClientFactory.getCluster().block()); - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), - reactiveResourceHolder); - org.springframework.transaction.support.TransactionSynchronizationManager - .unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); - org.springframework.transaction.support.TransactionSynchronizationManager - .bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction = new ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject( - reactiveResourceHolder); - setTransaction(transaction); - - /* end spring-data-couchbase transaction 1/2 */ - - Mono result = TransactionSynchronizationManager.forCurrentTransaction().flatMap((sm) -> { - sm.unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); - sm.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), - reactiveResourceHolder); - CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(transaction, true, false, false, true, null, sm); - prepareSynchronization(status, new CouchbaseTransactionDefinition()); - // System.err.println("deferContextual.ctx : " + xxx); - //Mono cxView = Mono.deferContextual(cx -> { System.err.println("CallbackTransactionManager.cx: "+cx); return Mono.just(cx);}); - try { - // Since we are on a different thread now transparently, at least make sure - // that the original method invocation is synchronized. - synchronized (this) { - execResult.set(callback.doInTransaction(status)); - } - } catch (RuntimeException e) { - throw e; - } catch (Throwable e) { - throw new RuntimeException(e); - } - return Mono.empty(); - }).contextWrite(TransactionContextManager.getOrCreateContext()) // this doesn't create a context on the desired publisher - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); - - result.onErrorResume(err -> { - AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), - "caught exception '%s' in async, rethrowing", err); - return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); - }).thenReturn(ctx); - return result.then(Mono.just(synchronizationManager)); - }); - /* begin spring-data-couchbase transaction 2/2 */ // this doesn't create a context on the desired publisher - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); - /* end spring-data-couchbase transaction 2/2 */ - }).doOnSubscribe(v -> startTime.set(System.nanoTime())); - - txnResult.block(); - return execResult.get(); // transactions.reactive().executeTransaction(merged,overall,ob).doOnNext(v->overall.span().finish()).doOnError(err->overall.span().failWith(err));}); - - } - - private void setTransaction(ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction) { - this.transactions = transactions; - } - - @Override - protected ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject doGetTransaction() - throws TransactionException { - /* - CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager - .getResource(template.getCouchbaseClientFactory()); - return new CouchbaseTransactionManager.CouchbaseTransactionObject(resourceHolder); - */ - return (ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject) transaction; - } - - @Override - protected boolean isExistingTransaction(Object transaction) throws TransactionException { - return extractTransaction(transaction).hasResourceHolder(); - } - - @Override - protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { - LOGGER.debug("Beginning Couchbase Transaction with Definition {}", definition); - } - - @Override - protected void doCommit(DefaultTransactionStatus status) throws TransactionException { - LOGGER.debug("Committing Couchbase Transaction with status {}", status); - } - - @Override - protected void doRollback(DefaultTransactionStatus status) throws TransactionException { - LOGGER.warn("Rolling back Couchbase Transaction with status {}", status); - org.springframework.transaction.support.TransactionSynchronizationManager - .unbindResource(reactiveCouchbaseClientFactory); - } - - @Override - protected void doCleanupAfterCompletion(Object transaction) { - LOGGER.trace("Performing cleanup of Couchbase Transaction {}", transaction); - org.springframework.transaction.support.TransactionSynchronizationManager - .unbindResource(reactiveCouchbaseClientFactory); - return; - } - - @Override - public void destroy() { - transactions.close(); - } - - @Override - public Object getResourceFactory() { - return reactiveTemplate.getCouchbaseClientFactory(); - } - - private static CouchbaseTransactionObject extractTransaction(Object transaction) { - Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, - () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, - transaction.getClass())); - - return (CouchbaseTransactionObject) transaction; - } - /* - public class CouchbaseResourceHolder extends ResourceHolderSupport { - - private volatile AttemptContextReactive attemptContext; - //private volatile TransactionResultMap resultMap = new TransactionResultMap(template); - - public CouchbaseResourceHolder(AttemptContextReactive attemptContext) { - this.attemptContext = attemptContext; - } - - public AttemptContextReactive getAttemptContext() { - return attemptContext; - } - - public void setAttemptContext(AttemptContextReactive attemptContext) { - this.attemptContext = attemptContext; - } - - //public TransactionResultMap getTxResultMap() { - // return resultMap; - //} - - @Override - public String toString() { - return "CouchbaseResourceHolder{" + "attemptContext=" + attemptContext + "}"; - } - } - - */ - - protected static class CouchbaseTransactionObject implements SmartTransactionObject { - - private final CouchbaseResourceHolder resourceHolder; - - CouchbaseTransactionObject(CouchbaseResourceHolder resourceHolder) { - this.resourceHolder = resourceHolder; - } - - @Override - public boolean isRollbackOnly() { - return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); - } - - @Override - public void flush() { - TransactionSynchronizationUtils.triggerFlush(); - } - - public boolean hasResourceHolder() { - return resourceHolder != null; - } - - @Override - public String toString() { - return "CouchbaseTransactionObject{" + "resourceHolder=" + resourceHolder + '}'; - } - } - - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); - } - -} +///* +// * Copyright 2021 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.transaction; +// +//import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +//import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +//import com.couchbase.client.java.transactions.TransactionResult; +//import reactor.core.publisher.Mono; +// +//import java.time.Duration; +//import java.time.temporal.ChronoUnit; +//import java.util.concurrent.atomic.AtomicReference; +// +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.DisposableBean; +//import org.springframework.data.couchbase.CouchbaseClientFactory; +//import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +//import org.springframework.data.couchbase.core.CouchbaseTemplate; +//import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +//import org.springframework.transaction.TransactionDefinition; +//import org.springframework.transaction.TransactionException; +//import org.springframework.transaction.reactive.TransactionContextManager; +//import org.springframework.transaction.reactive.TransactionSynchronizationManager; +//import org.springframework.transaction.support.AbstractPlatformTransactionManager; +//import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +//import org.springframework.transaction.support.DefaultTransactionStatus; +//import org.springframework.transaction.support.ResourceTransactionManager; +//import org.springframework.transaction.support.SmartTransactionObject; +//import org.springframework.transaction.support.TransactionCallback; +//import org.springframework.transaction.support.TransactionSynchronizationUtils; +//import org.springframework.util.Assert; +// +///** +// * Blocking TransactionManager +// * +// * @author Michael Nitschinger +// * @author Michael Reiche +// */ +// +//public class CouchbaseCallbackTransactionManager extends AbstractPlatformTransactionManager +// implements DisposableBean, ResourceTransactionManager, CallbackPreferringPlatformTransactionManager { +// +// private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); +// +// private final CouchbaseTemplate template; +// private final ReactiveCouchbaseTemplate reactiveTemplate; +// private final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; +// private final CouchbaseClientFactory couchbaseClientFactory; +// +// private ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction; +// +// public CouchbaseCallbackTransactionManager(CouchbaseTemplate template, ReactiveCouchbaseTemplate reactiveTemplate) { +// this.template = template; +// this.reactiveTemplate = reactiveTemplate; +// this.reactiveCouchbaseClientFactory = this.reactiveTemplate.getCouchbaseClientFactory(); +// this.couchbaseClientFactory = this.template.getCouchbaseClientFactory(); +// } +// +// public ReactiveCouchbaseTemplate template() { +// return reactiveTemplate; +// } +// +// private CouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options, +// ReactiveTransactionAttemptContext atr) { +// +// CouchbaseClientFactory databaseFactory = template.getCouchbaseClientFactory(); +// +// CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder( +// databaseFactory.getSession(options, atr), databaseFactory); +// return resourceHolder; +// } +// +// @Override +// public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { +// final AtomicReference execResult = new AtomicReference<>(); +// AtomicReference startTime = new AtomicReference<>(0L); +// +// Mono txnResult = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { +// /* begin spring-data-couchbase transaction 1/2 */ +// ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory +// .getSession(ClientSessionOptions.builder().causallyConsistent(true).build()) +// .block(); +// ReactiveCouchbaseResourceHolder reactiveResourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, +// reactiveCouchbaseClientFactory); +// +// CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(clientSession, +// template.getCouchbaseClientFactory()); +// +// Mono sync = TransactionContextManager.currentContext() +// .map(TransactionSynchronizationManager::new) +// . flatMap(synchronizationManager -> { +// System.err.println("CallbackTransactionManager: " + this); +// System.err.println("bindResource: " + reactiveCouchbaseClientFactory.getCluster().block()); +// // todo gp not sure why we bind, unbind, bind again? +// synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), +// reactiveResourceHolder); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); +// ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction = new ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject( +// reactiveResourceHolder); +// setTransaction(transaction); +// +// // todo gp experimenting with replacing the ClientSession, the ReactiveCouchbaseTransactionObject, +// // the resource holders etc., with just storing the TransactionAttemptContext. +// synchronizationManager.bindResource(ReactiveTransactionAttemptContext.class, ctx); +// +// /* end spring-data-couchbase transaction 1/2 */ +// +// // todo gp do we need TransactionSynchronizationManager.forCurrentTransaction()? as we already have synchronizationManager +// Mono result = TransactionSynchronizationManager.forCurrentTransaction().flatMap((sm) -> { +// // todo gp not sure why re-binding again? +// sm.unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); +// sm.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), +// reactiveResourceHolder); +// CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(transaction, true, false, false, true, null, sm); +// prepareSynchronization(status, new CouchbaseTransactionDefinition()); +// // System.err.println("deferContextual.ctx : " + xxx); +// //Mono cxView = Mono.deferContextual(cx -> { System.err.println("CallbackTransactionManager.cx: "+cx); return Mono.just(cx);}); +// try { +// // Since we are on a different thread now transparently, at least make sure +// // that the original method invocation is synchronized. +// synchronized (this) { +// // todo gp this will execute the lambda, and so we likely don't want that to be inside a synchronized block +// execResult.set(callback.doInTransaction(status)); +// } +// } catch (RuntimeException e) { +// throw e; +// } catch (Throwable e) { +// throw new RuntimeException(e); +// } +// return Mono.empty(); +// }).contextWrite(TransactionContextManager.getOrCreateContext()) // this doesn't create a context on the desired publisher +// .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); +// +// // todo gp this isn't part of the chain (no `result = result.onErrorResume...`) so isn't called +// // and presumably isn't needed? +//// result.onErrorResume(err -> { +//// AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), +//// "caught exception '%s' in async, rethrowing", err); +//// return Mono.error(ctx.TransactionOperationFailedException.convertToOperationFailedIfNeeded(err, ctx)); +//// }).thenReturn(ctx); +// +// return result.then(Mono.just(synchronizationManager)); +// }); +// /* begin spring-data-couchbase transaction 2/2 */ // this doesn't create a context on the desired publisher +// return sync.contextWrite(TransactionContextManager.getOrCreateContext()) +// .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); +// /* end spring-data-couchbase transaction 2/2 */ +// }).doOnSubscribe(v -> startTime.set(System.nanoTime())); +// +// txnResult.block(); +// return execResult.get(); // transactions.reactive().executeTransaction(merged,overall,ob).doOnNext(v->overall.span().finish()).doOnError(err->overall.span().failWith(err));}); +// +// } +// +// private void setTransaction(ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction) { +// this.transaction = transaction; +// } +// +// @Override +// protected ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject doGetTransaction() +// throws TransactionException { +// /* +// CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager +// .getResource(template.getCouchbaseClientFactory()); +// return new CouchbaseTransactionManager.CouchbaseTransactionObject(resourceHolder); +// */ +// return (ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject) transaction; +// } +// +// @Override +// protected boolean isExistingTransaction(Object transaction) throws TransactionException { +// return extractTransaction(transaction).hasResourceHolder(); +// } +// +// @Override +// protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { +// LOGGER.debug("Beginning Couchbase Transaction with Definition {}", definition); +// } +// +// @Override +// protected void doCommit(DefaultTransactionStatus status) throws TransactionException { +// LOGGER.debug("Committing Couchbase Transaction with status {}", status); +// } +// +// @Override +// protected void doRollback(DefaultTransactionStatus status) throws TransactionException { +// LOGGER.warn("Rolling back Couchbase Transaction with status {}", status); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .unbindResource(reactiveCouchbaseClientFactory); +// } +// +// @Override +// protected void doCleanupAfterCompletion(Object transaction) { +// LOGGER.trace("Performing cleanup of Couchbase Transaction {}", transaction); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .unbindResource(reactiveCouchbaseClientFactory); +// return; +// } +// +// @Override +// public void destroy() { +// } +// +// @Override +// public Object getResourceFactory() { +// return reactiveTemplate.getCouchbaseClientFactory(); +// } +// +// private static CouchbaseTransactionObject extractTransaction(Object transaction) { +// Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, +// () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, +// transaction.getClass())); +// +// return (CouchbaseTransactionObject) transaction; +// } +// /* +// public class CouchbaseResourceHolder extends ResourceHolderSupport { +// +// private volatile ReactiveTransactionAttemptContext attemptContext; +// //private volatile TransactionResultMap resultMap = new TransactionResultMap(template); +// +// public CouchbaseResourceHolder(ReactiveTransactionAttemptContext attemptContext) { +// this.attemptContext = attemptContext; +// } +// +// public ReactiveTransactionAttemptContext getAttemptContext() { +// return attemptContext; +// } +// +// public void setAttemptContext(ReactiveTransactionAttemptContext attemptContext) { +// this.attemptContext = attemptContext; +// } +// +// //public TransactionResultMap getTxResultMap() { +// // return resultMap; +// //} +// +// @Override +// public String toString() { +// return "CouchbaseResourceHolder{" + "attemptContext=" + attemptContext + "}"; +// } +// } +// +// */ +// +// protected static class CouchbaseTransactionObject implements SmartTransactionObject { +// +// private final CouchbaseResourceHolder resourceHolder; +// +// CouchbaseTransactionObject(CouchbaseResourceHolder resourceHolder) { +// this.resourceHolder = resourceHolder; +// } +// +// @Override +// public boolean isRollbackOnly() { +// return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); +// } +// +// @Override +// public void flush() { +// TransactionSynchronizationUtils.triggerFlush(); +// } +// +// public boolean hasResourceHolder() { +// return resourceHolder != null; +// } +// +// @Override +// public String toString() { +// return "CouchbaseTransactionObject{" + "resourceHolder=" + resourceHolder + '}'; +// } +// } +// +// private static Duration now() { +// return Duration.of(System.nanoTime(), ChronoUnit.NANOS); +// } +// +//} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java index 43a4b159c..16ab949a7 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java @@ -33,7 +33,7 @@ * @see CouchbaseTransactionManager * @see CouchbaseTemplate */ - +// todo gp understand why this is needed - can we not just hold ctx in Mono context? public class CouchbaseResourceHolder extends ResourceHolderSupport { private @Nullable ClientSession session; // which holds the atr diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java new file mode 100644 index 000000000..676ee2a74 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java @@ -0,0 +1,92 @@ +/* + * Copyright 2021 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.transaction; + +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; +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.CallbackPreferringPlatformTransactionManager; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import reactor.core.publisher.Mono; + +import java.util.concurrent.atomic.AtomicReference; + +// todo gp experimenting with simplest possible CallbackPreferringPlatformTransactionManager, extending PlatformTransactionManager +// not AbstractPlatformTransactionManager +public class CouchbaseSimpleCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); + + private final CouchbaseClientFactory couchbaseClientFactory; + + public CouchbaseSimpleCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory) { + this.couchbaseClientFactory = couchbaseClientFactory; + } + + @Override + public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { + final AtomicReference execResult = new AtomicReference<>(); + + couchbaseClientFactory.getCluster().transactions().run(ctx -> { + CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(null, true, false, false, true, null, null); + + // Setting ThreadLocal storage + TransactionSynchronizationManager.setActualTransactionActive(true); + TransactionSynchronizationManager.initSynchronization(); + // Oddly, TransactionSynchronizationManager.clear() does not clear resources + try { + TransactionSynchronizationManager.unbindResource(TransactionAttemptContext.class); + } + // todo gp must be a nicer way... + catch (IllegalStateException err) {} + TransactionSynchronizationManager.bindResource(TransactionAttemptContext.class, ctx); + + try { + execResult.set(callback.doInTransaction(status)); + } + finally { + TransactionSynchronizationManager.clear(); + } + }); + + TransactionSynchronizationManager.clear(); + + return execResult.get(); + } + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + return null; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + System.out.println("commit"); + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + System.out.println("rollback"); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleTransactionManager.java new file mode 100644 index 000000000..1947cb7a2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleTransactionManager.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018-2021 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.transaction; + +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionSystemException; + +// todo gp experimenting with the simplest possible class, extending PlatformTransactionManager not AbstractPlatformTransactionManager +public class CouchbaseSimpleTransactionManager implements PlatformTransactionManager { + + private final CouchbaseClientFactory clientFactory; + + public CouchbaseSimpleTransactionManager(CouchbaseClientFactory clientFactory) { + this.clientFactory = clientFactory; + } + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + return null; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + // todo gp what here - do we need to re-allow explicit commit? how to handle retries of this part? + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + // todo gp same as commit() + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java index aeccf9f4e..14f0d441a 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java @@ -1,5 +1,8 @@ package org.springframework.data.couchbase.transaction; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; +import com.couchbase.client.java.transactions.TransactionResult; import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import reactor.core.publisher.Mono; @@ -20,10 +23,6 @@ import org.springframework.util.Assert; import com.couchbase.client.core.error.CouchbaseException; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.TransactionGetResult; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.TransactionsReactive; public class CouchbaseStuffHandle { @@ -33,7 +32,7 @@ public class CouchbaseStuffHandle { private final TransactionDefinition transactionDefinition; Map getResultMap = new HashMap<>(); - private AttemptContextReactive attemptContextReactive; + private ReactiveTransactionAttemptContext attemptContextReactive; public CouchbaseStuffHandle() { transactionManager = null; @@ -61,13 +60,15 @@ public Mono reactive(FunctionPerTransactionConfig. */ public Mono reactive(Function> transactionLogic, - boolean commit) { - return ((ReactiveCouchbaseTransactionManager) transactionManager).getTransactions().reactive((ctx) -> { - setAttemptContextReactive(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport - // for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method - // and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder() - return transactionLogic.apply(this); - }/*, commit*/); + boolean commit) { + // todo gp this needs access to a Cluster + return Mono.empty(); +// return ((ReactiveCouchbaseTransactionManager) transactionManager).getTransactions().reactive((ctx) -> { +// setAttemptContextReactive(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport +// // for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method +// // and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder() +// return transactionLogic.apply(this); +// }/*, commit*/); } public TransactionResultHolder transactionResultHolder(Integer key) { @@ -80,7 +81,7 @@ public TransactionResultHolder transactionResultHolder(TransactionGetResult resu return holder; } - public void setAttemptContextReactive(AttemptContextReactive attemptContextReactive) { + public void setAttemptContextReactive(ReactiveTransactionAttemptContext attemptContextReactive) { this.attemptContextReactive = attemptContextReactive; // see ReactiveCouchbaseTransactionManager.doBegin() // transactionManager.getReactiveTransaction(new CouchbaseTransactionDefinition()).block(); @@ -94,7 +95,7 @@ public void setAttemptContextReactive(AttemptContextReactive attemptContextReact */ } - public AttemptContextReactive getAttemptContextReactive() { + public ReactiveTransactionAttemptContext getAttemptContextReactive() { return attemptContextReactive; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java index 006df028f..d46d73bc7 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java @@ -1,17 +1,18 @@ package org.springframework.data.couchbase.transaction; -import com.couchbase.transactions.AttemptContext; -import com.couchbase.transactions.AttemptContextReactive; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.DefaultTransactionDefinition; public class CouchbaseTransactionDefinition extends DefaultTransactionDefinition { - AttemptContextReactive atr; - AttemptContext at; + ReactiveTransactionAttemptContext atr; + TransactionAttemptContext at; public CouchbaseTransactionDefinition(){ super(); + setIsolationLevel(ISOLATION_READ_COMMITTED); } public CouchbaseTransactionDefinition(TransactionDefinition that) { @@ -22,15 +23,15 @@ public CouchbaseTransactionDefinition(int propagationBehavior) { super(propagationBehavior); } - public void setAttemptContextReactive(AttemptContextReactive atr){ + public void setAttemptContextReactive(ReactiveTransactionAttemptContext atr){ this.atr = atr; } - public AttemptContextReactive getAttemptContextReactive(){ + public ReactiveTransactionAttemptContext getAttemptContextReactive(){ return atr; } - public void setAttemptContext(AttemptContext attemptContext) { + public void setAttemptContext(TransactionAttemptContext attemptContext) { at = attemptContext; } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java index 840903624..e9fc558a3 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.transaction; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.lang.Nullable; @@ -32,9 +34,6 @@ import org.springframework.util.ClassUtils; import com.couchbase.client.core.error.CouchbaseException; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; import reactor.core.publisher.Mono; /** @@ -63,12 +62,11 @@ * @see MongoDB Transaction Documentation * @see MongoDatabaseUtils#getDatabase(CouchbaseClientFactory, SessionSynchronization) */ +// todo gp is this needed, or can we only have the CallbackPreferring one? public class CouchbaseTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { private @Nullable CouchbaseClientFactory databaseFactory; - private @Nullable Transactions transactions; // This is the com.couchbase.transactions object - private @Nullable TransactionConfig config; private @Nullable TransactionOptions options; /** @@ -86,28 +84,18 @@ public class CouchbaseTransactionManager extends AbstractPlatformTransactionMana */ public CouchbaseTransactionManager() {} - /** - * Create a new {@link CouchbaseTransactionManager} obtaining sessions from the given {@link CouchbaseClientFactory}. - * - * @param databaseFactory must not be {@literal null}. - */ - public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory) { - this(databaseFactory, null); - } - /** * Create a new {@link CouchbaseTransactionManager} obtaining sessions from the given {@link CouchbaseClientFactory} * applying the given {@link TransactionOptions options}, if present, when starting a new transaction. * * @param databaseFactory must not be {@literal null}. @//param options can be {@literal null}. */ - public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory, @Nullable Transactions transactions) { + public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory) { Assert.notNull(databaseFactory, "DbFactory must not be null!"); System.err.println(this); System.err.println(databaseFactory.getCluster()); this.databaseFactory = databaseFactory; - this.transactions = transactions; } /* @@ -351,12 +339,12 @@ public void afterPropertiesSet() { } private CouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options, - AttemptContextReactive atr) { + ReactiveTransactionAttemptContext atr) { CouchbaseClientFactory databaseFactory = getResourceFactory(); CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder( - databaseFactory.getSession(options, transactions, null, atr), databaseFactory); + databaseFactory.getSession(options, atr), databaseFactory); // TODO resourceHolder.setTimeoutIfNotDefaulted(determineTimeout(definition)); return resourceHolder; @@ -427,10 +415,6 @@ public CouchbaseClientFactory getDatabaseFactory() { return databaseFactory; } - public Transactions getTransactions() { - return transactions; - } - /** * MongoDB specific transaction object, representing a {@link CouchbaseResourceHolder}. Used as transaction object by * {@link CouchbaseTransactionManager}. diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save index aaab01580..200463ede 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.transaction; -import com.couchbase.transactions.AttemptContext; +import com.couchbase.transactions.TransactionAttemptContext; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionManager; @@ -53,7 +53,7 @@ public class CouchbaseTransactionalOperatorNonReactive implements TransactionalO private final TransactionDefinition transactionDefinition; Map getResultMap = new HashMap<>(); - private AttemptContext attemptContext; + private TransactionAttemptContext attemptContext; public CouchbaseTransactionalOperatorNonReactive(CouchbaseTransactionManager transactionManager) { this(transactionManager, new CouchbaseTransactionDefinition()); @@ -95,11 +95,11 @@ public class CouchbaseTransactionalOperatorNonReactive implements TransactionalO return holder; } - public void setAttemptContext(AttemptContext attemptContext) { + public void setAttemptContext(TransactionAttemptContext attemptContext) { this.attemptContext = attemptContext; } - public AttemptContext getAttemptContext() { + public TransactionAttemptContext getAttemptContext() { return attemptContext; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java index f7c76ff2a..d73ba2057 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java @@ -33,6 +33,7 @@ * @see ReactiveCouchbaseTransactionManager * @see ReactiveCouchbaseTemplate */ +// todo gp understand why this is needed public class ReactiveCouchbaseResourceHolder extends ResourceHolderSupport { private @Nullable ClientSession session; // which holds the atr diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java index b390c8151..cb8705fd1 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java @@ -33,10 +33,6 @@ import org.springframework.util.ClassUtils; import com.couchbase.client.core.error.CouchbaseException; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.TransactionQueryOptions; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; /** * A {@link org.springframework.transaction.ReactiveTransactionManager} implementation that manages @@ -69,8 +65,6 @@ public class ReactiveCouchbaseTransactionManager extends AbstractReactiveTransac implements InitializingBean { private @Nullable ReactiveCouchbaseClientFactory databaseFactory; // (why) does this need to be reactive? - private @Nullable Transactions transactions; // This is the com.couchbase.transactions object - private @Nullable TransactionConfig config; /** * Create a new {@link ReactiveCouchbaseTransactionManager} for bean-style usage. @@ -92,14 +86,11 @@ public ReactiveCouchbaseTransactionManager() {} * starting a new transaction. * * @param databaseFactory must not be {@literal null}. - * @param transactions - couchbase Transactions object */ - public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databaseFactory, - @Nullable Transactions transactions) { + public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databaseFactory) { Assert.notNull(databaseFactory, "DatabaseFactory must not be null!"); this.databaseFactory = databaseFactory; // should be a clone? TransactionSynchronizationManager binds objs to it - this.transactions = transactions; - System.err.println("ReactiveCouchbaseTransactionManager : created Transactions: " + transactions); + System.err.println("ReactiveCouchbaseTransactionManager : created"); } /* @@ -111,10 +102,6 @@ public ReactiveCouchbaseTransactionManager(CouchbaseClientFactory databaseFactor System.err.println("ReactiveCouchbaseTransactionManager : created Transactions: " + transactions); } */ - public Transactions getTransactions() { - System.err.println("ReactiveCouchbaseTransactionManager.getTransactions() : " + transactions); - return transactions; - } /* * (non-Javadoc) @@ -168,7 +155,7 @@ protected Mono doBegin(TransactionSynchronizationManager synchronizationMa }).doOnNext(resourceHolder -> { - couchbaseTransactionObject.startTransaction(config); + couchbaseTransactionObject.startTransaction(); if (logger.isDebugEnabled()) { logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession()))); @@ -330,15 +317,6 @@ public void setDatabaseFactory(ReactiveCouchbaseClientFactory databaseFactory) { this.databaseFactory = databaseFactory; } - /** - * Set the {@link TransactionConfig} to be applied when starting transactions. - * - * @param config can be {@literal null}. - */ - public void setConfig(@Nullable TransactionConfig config) { - this.config = config; - } - /** * Get the {@link CouchbaseClientFactory} that this instance manages transactions for. * @@ -363,9 +341,7 @@ private Mono newResourceHolder(TransactionDefin ReactiveCouchbaseClientFactory dbFactory = getRequiredDatabaseFactory(); // TODO MSR : config should be derived from config that was used for `transactions` - getTransactions().reactive(); - TransactionConfig config = transactions.reactive().config(); - Mono sess = Mono.just(dbFactory.getSession(options, transactions, config , null/* TODO */)); + Mono sess = dbFactory.getSession(options); return sess.map(session -> new ReactiveCouchbaseResourceHolder(session, dbFactory)); } @@ -463,17 +439,13 @@ final boolean hasResourceHolder() { /** * Start a XxxxxxXX transaction optionally given {@link TransactionQueryOptions}. - * + * todo gp how to expose TransactionOptions * @param options can be {@literal null} */ - void startTransaction(@Nullable TransactionConfig options) { + void startTransaction() { ClientSession session = getRequiredSession(); - if (options != null) { - session.startTransaction(options); - } else { - session.startTransaction(); - } + session.startTransaction(); } /** diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java deleted file mode 100644 index 86b23d9c0..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/TransactionOptions.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2021 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.transaction; - -public class TransactionOptions { -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java index e3db6bd1a..2f86a7563 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java @@ -1,18 +1,9 @@ package org.springframework.data.couchbase.transaction; import com.couchbase.client.java.env.ClusterEnvironment; -import com.couchbase.transactions.AttemptContextReactive; -import com.couchbase.transactions.AttemptContextReactiveAccessor; -import com.couchbase.transactions.TransactionContext; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.TransactionsReactive; -import com.couchbase.transactions.config.MergedTransactionConfig; -import com.couchbase.transactions.config.PerTransactionConfig; -import com.couchbase.transactions.config.PerTransactionConfigBuilder; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; -import com.couchbase.transactions.error.external.TransactionOperationFailed; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.transaction.ReactiveTransaction; @@ -28,94 +19,95 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +// todo gp needed now Transactions has gone? public class TransactionsWrapper { - Transactions transactions; ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; - public TransactionsWrapper(Transactions transactions, ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory){ - this.transactions = transactions; + public TransactionsWrapper(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory){ this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory; } /** * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default PerTransactionConfig. */ - public Mono reactive(Function> transactionLogic) { + public Mono reactive(Function> transactionLogic) { // TODO long duration for debugger Duration duration = Duration.ofMinutes(20); System.err.println("tx duration of "+duration); - return run(transactionLogic, PerTransactionConfigBuilder.create().expirationTime(duration).build()); + return run(transactionLogic, TransactionOptions.transactionOptions().timeout(duration)); } - public Mono run(Function> transactionLogic, - PerTransactionConfig perConfig) { - TransactionConfig config = TransactionConfigBuilder.create().build(); - - ClusterEnvironment env = ClusterEnvironment.builder().build(); - return Mono.defer(() -> { - MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); - - TransactionContext overall = - new TransactionContext(env.requestTracer(), - env.eventBus(), - UUID.randomUUID().toString(), - now(), - Duration.ZERO, - merged); - AtomicReference startTime = new AtomicReference<>(0L); - - Mono ob = Mono.fromCallable(() -> { - String txnId = UUID.randomUUID().toString(); - overall.LOGGER.info(configDebug(config, perConfig)); - return transactions.reactive().createAttemptContext(overall, merged, txnId); - }).flatMap(ctx -> { - - AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", - overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); - -/* begin spring-data-couchbase transaction 1/2 */ - ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, - reactiveCouchbaseClientFactory); - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); -/* end spring-data-couchbase transaction 1/2 */ - Mono result = transactionLogic.apply(ctx); - result - .onErrorResume(err -> { - AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); - logElidedStacktrace(ctx, err); - - return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); - }) - .thenReturn(ctx); - return result.then(Mono.just(synchronizationManager)); - }); - /* begin spring-data-couchbase transaction 2/2 */ - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(Mono.just(ctx)); -/* end spring-data-couchbase transaction 2/2 */ - }).doOnSubscribe(v -> startTime.set(System.nanoTime())) - .doOnNext(v -> AttemptContextReactiveAccessor.getLogger(v).trace(v.attemptId(), "finished attempt %d in %sms", - overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); - - return transactions.reactive().executeTransaction(merged, overall, ob) - .doOnNext(v -> overall.span().finish()) - .doOnError(err -> overall.span().failWith(err)); - }); - } - - private void logElidedStacktrace(AttemptContextReactive ctx, Throwable err) { - transactions.reactive().logElidedStacktrace(ctx, err); - } - - private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { - return transactions.reactive().configDebug(config, perConfig); + public Mono run(Function> transactionLogic, + TransactionOptions perConfig) { + // todo gp this is duplicating a lot of logic from the core loop, and is hopefully not needed.. + return Mono.empty(); +// TransactionConfig config = TransactionConfigBuilder.create().build(); +// +// ClusterEnvironment env = ClusterEnvironment.builder().build(); +// return Mono.defer(() -> { +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// +// TransactionContext overall = +// new TransactionContext(env.requestTracer(), +// env.eventBus(), +// UUID.randomUUID().toString(), +// now(), +// Duration.ZERO, +// merged); +// AtomicReference startTime = new AtomicReference<>(0L); +// +// Mono ob = Mono.fromCallable(() -> { +// String txnId = UUID.randomUUID().toString(); +// overall.LOGGER.info(configDebug(config, perConfig)); +// return transactions.reactive().createAttemptContext(overall, merged, txnId); +// }).flatMap(ctx -> { +// +// AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", +// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); +// +///* begin spring-data-couchbase transaction 1/2 */ +// ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory +// .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); +// ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, +// reactiveCouchbaseClientFactory); +// Mono sync = TransactionContextManager.currentContext() +// .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { +// synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); +// prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); +///* end spring-data-couchbase transaction 1/2 */ +// Mono result = transactionLogic.apply(ctx); +// result +// .onErrorResume(err -> { +// AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); +// logElidedStacktrace(ctx, err); +// +// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); +// }) +// .thenReturn(ctx); +// return result.then(Mono.just(synchronizationManager)); +// }); +// /* begin spring-data-couchbase transaction 2/2 */ +// return sync.contextWrite(TransactionContextManager.getOrCreateContext()) +// .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(Mono.just(ctx)); +///* end spring-data-couchbase transaction 2/2 */ +// }).doOnSubscribe(v -> startTime.set(System.nanoTime())) +// .doOnNext(v -> AttemptContextReactiveAccessor.getLogger(v).trace(v.attemptId(), "finished attempt %d in %sms", +// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); +// +// return transactions.reactive().executeTransaction(merged, overall, ob) +// .doOnNext(v -> overall.span().finish()) +// .doOnError(err -> overall.span().failWith(err)); +// }); } +// private void logElidedStacktrace(ReactiveTransactionAttemptContext ctx, Throwable err) { +// transactions.reactive().logElidedStacktrace(ctx, err); +// } +// +// private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { +// return transactions.reactive().configDebug(config, perConfig); +// } +// private static Duration now() { return Duration.of(System.nanoTime(), ChronoUnit.NANOS); } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java b/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java index a82cdf333..c32711b60 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java @@ -6,8 +6,8 @@ package org.springframework.data.couchbase.transaction.internal; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.data.couchbase.transaction.TransactionOptions; public interface AsyncClientSession extends ClientSession { boolean hasActiveTransaction(); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index 7ee1c3862..1543211a8 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -22,7 +22,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.error.TransactionFailedException; import lombok.Data; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -62,7 +66,6 @@ import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.couchbase.transaction.ClientSessionOptions; -import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; @@ -95,13 +98,6 @@ import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.RemoveOptions; -import com.couchbase.transactions.TransactionDurabilityLevel; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; -import com.couchbase.transactions.error.TransactionFailed; -import com.couchbase.transactions.error.external.TransactionOperationFailed; /** * Tests for com.couchbase.transactions using @@ -118,12 +114,11 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT @Autowired CouchbaseClientFactory couchbaseClientFactory; @Autowired ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; - @Autowired CouchbaseTransactionManager couchbaseTransactionManager; + //@Autowired CouchbaseTransactionManager couchbaseTransactionManager; @Autowired ReactivePersonRepository rxRepo; @Autowired PersonRepository repo; @Autowired CouchbaseTemplate cbTmpl; @Autowired ReactiveCouchbaseTemplate rxCBTmpl; - @Autowired Transactions transactions; /* DO NOT @Autowired - it will result in no @Transactional annotation behavior */ PersonService personService; @Autowired CouchbaseTemplate operations; @@ -133,13 +128,14 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT public static void beforeAll() { callSuperBeforeAll(new Object() {}); context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionIntegrationTests.Config.class, - PersonService.class, CouchbasePersonTransactionIntegrationTests.TransactionInterception.class); +// PersonService.class, CouchbasePersonTransactionIntegrationTests.TransactionInterception.class); + PersonService.class); } @AfterAll public static void afterAll() { callSuperAfterAll(new Object() {}); - context.close(); + //context.close(); } @BeforeEach @@ -160,7 +156,7 @@ public void beforeEachTest() { } /* Not used in this class. The class itself is not @Transaction - + List>> assertionList; @BeforeTransaction @@ -234,6 +230,18 @@ public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { assertEquals(1, count, "should have saved and found 1"); } + @Test + public void replaceInTxAnnotatedCallback() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + cbTmpl.insertById(Person.class).one(person); + AtomicInteger tryCount = new AtomicInteger(0); + Person p = personService.declarativeFindReplacePersonCallback(person, tryCount); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + @Test public void commitShouldPersistTxEntriesOfTxAnnotatedMethodReactive() { Person p = new Person(null, "Walter", "White"); @@ -276,7 +284,8 @@ public void emitMultipleElementsDuringTransaction() { @Test public void errorAfterTxShouldNotAffectPreviousStep() { Person p = personService.savePerson(new Person(null, "Walter", "White")); - assertThrows(TransactionOperationFailed.class, () -> personService.savePerson(p)); + // todo gp user shouldn't be getting exposed to TransactionOperationFailedException + assertThrows(TransactionOperationFailedException.class, () -> personService.savePerson(p)); Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); assertEquals(1, count, "should have saved and found 1"); } @@ -289,9 +298,9 @@ public void errorAfterTxShouldNotAffectPreviousStep() { public void replacePersonCBTransactionsRxTmpl() { Person person = new Person(1, "Walter", "White"); cbTmpl.insertById(Person.class).one(person); - Mono result = transactions.reactive(ctx -> { // get the ctx + Mono result = this.couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, reactiveCouchbaseClientFactory); Mono sync = TransactionContextManager.currentContext() @@ -299,11 +308,6 @@ public void replacePersonCBTransactionsRxTmpl() { synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // - .flatMap((pp) -> { - System.err.println("==================================== ATTEMPT : " + ctx.attemptId() - + " ======================================"); - return Mono.just(pp); - }) // .flatMap((pp) -> rxCBTmpl.replaceById(Person.class).one(pp)) // .then(Mono.just(synchronizationManager)); // tx }); @@ -321,11 +325,11 @@ public void insertPersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); try { rxCBTmpl.removeById(Person.class).one(person.getId().toString()); - } catch (DocumentNotFoundException dnfe) {} - Mono result = transactions.reactive(ctx -> { // get the ctx + } catch(DocumentNotFoundException dnfe){} + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, reactiveCouchbaseClientFactory); Mono sync = TransactionContextManager.currentContext() @@ -339,7 +343,7 @@ public void insertPersonCBTransactionsRxTmplRollback() { .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); }); - assertThrowsCause(TransactionFailed.class, SimulateFailureException.class, (ignore) -> { + assertThrowsCause(TransactionFailedException.class, SimulateFailureException.class, (ignore) -> { result.block(); return null; }); @@ -351,9 +355,9 @@ public void insertPersonCBTransactionsRxTmplRollback() { public void insertTwicePersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); sleepMs(1000); - Mono result = transactions.reactive(ctx -> { // get the ctx + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, reactiveCouchbaseClientFactory); Mono sync = TransactionContextManager.currentContext() @@ -367,7 +371,7 @@ public void insertTwicePersonCBTransactionsRxTmplRollback() { return sync.contextWrite(TransactionContextManager.getOrCreateContext()) .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); }); - assertThrowsCause(TransactionFailed.class, DuplicateKeyException.class, (ignore) -> { + assertThrowsCause(TransactionFailedException.class, DuplicateKeyException.class, (ignore) -> { result.block(); return null; }); @@ -385,10 +389,10 @@ public void replaceWithCasConflictResolvedViaRetry() { cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - Mono result = transactions.reactive(ctx -> { // get the ctx + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get the ctx // see TransactionalOperatorImpl.transactional(). ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, reactiveCouchbaseClientFactory); Mono sync = TransactionContextManager.currentContext() @@ -398,7 +402,7 @@ public void replaceWithCasConflictResolvedViaRetry() { return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // .flatMap((ppp) -> { tryCount.getAndIncrement(); - System.err.println("===== ATTEMPT : " + tryCount.get() + " " + ctx.attemptId() + " ====="); + System.err.println("===== ATTEMPT : " + tryCount.get() + " ====="); return Mono.just(ppp); })// .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)) // @@ -429,7 +433,7 @@ public void wrapperReplaceWithCasConflictResolvedViaRetry() { t.start(); cbTmpl.insertById(Person.class).one(person); tryCount.set(0); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(transactions, reactiveCouchbaseClientFactory); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); Mono result = transactionsWrapper.reactive(ctx -> { System.err.println("try: " + tryCount.incrementAndGet()); return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // @@ -546,15 +550,15 @@ public void replacePersonCBTransactionsRxTmplRollback() { rxCBTmpl.insertById(Person.class).one(person).block(); sleepMs(1000); Mono result = transactions.reactive(((ctx) -> { // get the ctx - // can we take the AttemptContextReactive ctx and save it in the context? + // can we take the ReactiveTransactionAttemptContext ctx and save it in the context? ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(clientSession, couchbaseClientFactory); - // I think this needs to happen within the transactions.reactive() call - or equivalent. + // I think this needs to happen within the couchbaseClientFactory.getCluster().reactive().transactions().run() call - or equivalent. // this currentContext() call is going to create a new ctx, and store the acr. Will it get uses in syncFlatMap() - // below? Should the ctx be created in the above call to transactions.reactive()? + // below? Should the ctx be created in the above call to couchbaseClientFactory.getCluster().reactive().transactions().run()? // How does this work in savePerson etc? // is there means for just getting the currentContext() without creating it? Mono sync = TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new) @@ -575,7 +579,7 @@ public void replacePersonCBTransactionsRxTmplRollback() { result.block(); - // assertThrows(TransactionFailed.class, () -> result.block()); + // assertThrows(TransactionFailedException.class, () -> result.block()); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); System.err.println(pFound); assertEquals(person.getFirstname(), pFound.getFirstname()); @@ -624,7 +628,7 @@ public void deletePersonCBTransactionsRxTmplFail() { .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) .then(); })); - assertThrows(TransactionFailed.class, result::block); + assertThrows(TransactionFailedException.class, result::block); Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals(pFound, person, "Should have found " + person); } @@ -655,7 +659,7 @@ public void deletePersonCBTransactionsRxRepoFail() { return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); })); - assertThrows(TransactionFailed.class, result::block); + assertThrows(TransactionFailedException.class, result::block); Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals(pFound, person, "Should have found " + person); } @@ -694,7 +698,7 @@ public void insertPersonRbCBTransactions() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { e.printStackTrace(); if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); @@ -704,7 +708,7 @@ public void insertPersonRbCBTransactions() { e.printStackTrace(); } } - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -723,7 +727,7 @@ public void replacePersonRbCBTransactions() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals(person, pFound, "Should have found " + person); @@ -732,7 +736,7 @@ public void replacePersonRbCBTransactions() { e.printStackTrace(); } } - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -835,12 +839,6 @@ public String getBucketName() { return bucketName(); } - @Override - public TransactionConfig transactionConfig() { - // expirationTime 20 minutes for stepping with the debugger - return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofMinutes(20)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); - } /* beforeAll creates a PersonService bean in the applicationContext @@ -884,43 +882,44 @@ public String toString() { } } - @Configuration(proxyBeanMethods = false) - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static class TransactionInterception { - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource, - CouchbaseTransactionManager txManager) { - TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(); - interceptor.setTransactionAttributeSource(transactionAttributeSource); - if (txManager != null) { - interceptor.setTransactionManager(txManager); - } - return interceptor; - } - - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public TransactionAttributeSource transactionAttributeSource() { - return new AnnotationTransactionAttributeSource(); - } - - @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( - TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { - - BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); - advisor.setTransactionAttributeSource(transactionAttributeSource); - advisor.setAdvice(transactionInterceptor); - // if (this.enableTx != null) { - // advisor.setOrder(this.enableTx.getNumber("order")); - // } - return advisor; - } - - } + // todo gp disabled while trying to get alternative method of CouchbaseCallbackTransactionManager working +// @Configuration(proxyBeanMethods = false) +// @Role(BeanDefinition.ROLE_INFRASTRUCTURE) +// static class TransactionInterception { +// +// @Bean +// @Role(BeanDefinition.ROLE_INFRASTRUCTURE) +// public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource, +// CouchbaseTransactionManager txManager) { +// TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(); +// interceptor.setTransactionAttributeSource(transactionAttributeSource); +// if (txManager != null) { +// interceptor.setTransactionManager(txManager); +// } +// return interceptor; +// } +// +// @Bean +// @Role(BeanDefinition.ROLE_INFRASTRUCTURE) +// public TransactionAttributeSource transactionAttributeSource() { +// return new AnnotationTransactionAttributeSource(); +// } +// +// @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) +// @Role(BeanDefinition.ROLE_INFRASTRUCTURE) +// public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( +// TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { +// +// BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); +// advisor.setTransactionAttributeSource(transactionAttributeSource); +// advisor.setAdvice(transactionInterceptor); +// // if (this.enableTx != null) { +// // advisor.setOrder(this.enableTx.getNumber("order")); +// // } +// return advisor; +// } +// +// } @Service @Component @@ -930,16 +929,16 @@ public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( class PersonService { final CouchbaseOperations personOperations; - final CouchbaseTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; + final CouchbaseSimpleCallbackTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; final ReactiveCouchbaseOperations personOperationsRx; final ReactiveCouchbaseTransactionManager managerRx; - public PersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, + public PersonService(CouchbaseOperations ops, CouchbaseSimpleCallbackTransactionManager mgr, ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { personOperations = ops; manager = mgr; System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); - System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); +// System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); System.err.println("manager Manager : " + manager); personOperationsRx = opsRx; @@ -1039,7 +1038,7 @@ public Person declarativeSavePersonErrors(Person person) { return p; } - @Autowired CouchbaseCallbackTransactionManager callbackTm; + @Autowired CouchbaseSimpleCallbackTransactionManager callbackTm; /** * to execute while ThreadReplaceloop() is running should force a retry @@ -1047,19 +1046,20 @@ public Person declarativeSavePersonErrors(Person person) { * @param person * @return */ - @Transactional(transactionManager = BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); - System.err.println("declarativeFindReplacePersonCallback cluster : " - + callbackTm.template().getCouchbaseClientFactory().getCluster().block()); - System.err.println("declarativeFindReplacePersonCallback resourceHolder : " - + org.springframework.transaction.support.TransactionSynchronizationManager - .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); +// System.err.println("declarativeFindReplacePersonCallback cluster : " +// + callbackTm.template().getCouchbaseClientFactory().getCluster().block()); +// System.err.println("declarativeFindReplacePersonCallback resourceHolder : " +// + org.springframework.transaction.support.TransactionSynchronizationManager +// .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); Person p = personOperations.findById(Person.class).one(person.getId().toString()); return personOperations.replaceById(Person.class).one(p); } + /** * to execute while ThreadReplaceloop() is running should force a retry * diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java index 9da5bbb73..2c703f9b4 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -1,617 +1,609 @@ -/* - * Copyright 2012-2021 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.Cluster; -import lombok.Data; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; -import org.springframework.data.domain.Persistable; -import org.springframework.test.context.transaction.AfterTransaction; -import org.springframework.test.context.transaction.BeforeTransaction; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.domain.Person; -import org.springframework.data.couchbase.domain.PersonRepository; -import org.springframework.data.couchbase.domain.ReactivePersonRepository; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; -import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; -import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; -import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterType; -import org.springframework.data.couchbase.util.IgnoreWhen; -import org.springframework.data.couchbase.util.JavaIntegrationTests; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.reactive.TransactionalOperator; -import org.springframework.transaction.support.DefaultTransactionDefinition; - -import com.couchbase.client.core.cnc.Event; -import com.couchbase.client.core.error.DocumentNotFoundException; -import com.couchbase.client.java.Collection; -import com.couchbase.client.java.ReactiveCollection; -import com.couchbase.client.java.kv.RemoveOptions; -import com.couchbase.transactions.TransactionDurabilityLevel; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; - -import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for com.couchbase.transactions without using the spring data transactions framework - * - * @author Michael Reiche - */ -@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(CouchbasePersonTransactionReactiveIntegrationTests.Config.class) -//@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) -public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { - - @Autowired CouchbaseClientFactory couchbaseClientFactory; - @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; - @Autowired CouchbaseTransactionManager couchbaseTransactionManager; - @Autowired ReactivePersonRepository rxRepo; - @Autowired PersonRepository repo; - @Autowired ReactiveCouchbaseTemplate rxCBTmpl; - - @Autowired Cluster myCluster; - - /* DO NOT @Autowired */ PersonService personService; - - static GenericApplicationContext context; - @Autowired ReactiveCouchbaseTemplate operations; - - @BeforeAll - public static void beforeAll() { - callSuperBeforeAll(new Object() {}); - context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionReactiveIntegrationTests.Config.class, - CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); - } - - @AfterAll - public static void afterAll() { - callSuperAfterAll(new Object() {}); - } - - @BeforeEach - public void beforeEachTest() { - personService = context.getBean(CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); // getting it via autowired results in no @Transactional - operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); - operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); - operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); - operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); - } - - - @Test // DATAMONGO-2265 - public void shouldRollbackAfterException() { - personService.savePersonErrors(new Person(null, "Walter", "White")) // - .as(StepVerifier::create) // - .verifyError(RuntimeException.class); - // operations.findByQuery(Person.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); - // sleepMs(5000); - operations.count(new Query(), Person.class) // - .as(StepVerifier::create) // - .expectNext(0L) // - .verifyComplete(); - } - - @Test // DATAMONGO-2265 - // @Rollback(false) - public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { - Person p = new Person(null, "Walter", "White"); - try { - personService.declarativeSavePersonErrors(p) // - .as(StepVerifier::create) // - .expectComplete(); - // .verifyError(RuntimeException.class); - } catch (RuntimeException e) { - if (e instanceof SimulateFailureException || (e.getMessage() != null && e.getMessage().contains("poof"))) { - System.err.println(e); - } else { - throw e; - } - } - - } - - @Test // DATAMONGO-2265 - public void commitShouldPersistTxEntries() { - - personService.savePerson(new Person(null, "Walter", "White")) // - .as(StepVerifier::create) // - .expectNextCount(1) // - .verifyComplete(); - - operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count().block(); - - operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); - } - - @Test // DATAMONGO-2265 - public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { - - personService.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // - .expectNextCount(1) // - .verifyComplete(); - - operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); - - } - - @Test // DATAMONGO-2265 - public void commitShouldPersistTxEntriesAcrossCollections() { - - personService.saveWithLogs(new Person(null, "Walter", "White")) // - .then() // - .as(StepVerifier::create) // - .verifyComplete(); - - operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); - - operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // - .as(StepVerifier::create) // - .expectNext(4L) // - .verifyComplete(); - } - - @Test // DATAMONGO-2265 - public void rollbackShouldAbortAcrossCollections() { - - personService.saveWithErrorLogs(new Person(null, "Walter", "White")) // - .then() // - .as(StepVerifier::create) // - .verifyError(); - - operations.count(new Query(), Person.class) // - .as(StepVerifier::create) // - .expectNext(0L) // - .verifyComplete(); - - operations.count(new Query(), EventLog.class)// - .as(StepVerifier::create) // - .expectNext(0L) // - .verifyComplete(); - } - - @Test // DATAMONGO-2265 - public void countShouldWorkInsideTransaction() { - - personService.countDuringTx(new Person(null, "Walter", "White")) // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); - } - - @Test // DATAMONGO-2265 - public void emitMultipleElementsDuringTransaction() { - - try { - personService.saveWithLogs(new Person(null, "Walter", "White")) // - .as(StepVerifier::create) // - .expectNextCount(4L) // - .verifyComplete(); - } catch (Exception e) { - System.err.println("Done"); - throw e; - } - } - - @Test // DATAMONGO-2265 - public void errorAfterTxShouldNotAffectPreviousStep() { - - Person p = new Person(1, "Walter", "White"); - remove(couchbaseTemplate, "_default", p.getId().toString()); - personService.savePerson(p) // - //.delayElement(Duration.ofMillis(100)) // - .then(Mono.error(new RuntimeException("my big bad evil error"))).as(StepVerifier::create) // - .expectError() - .verify(); - //.expectError() // - //.as(StepVerifier::create) - //.expectNext(p) - //.verifyComplete(); - - operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); - } - - // @RequiredArgsConstructor - static class PersonService { - - final ReactiveCouchbaseOperations personOperationsRx; - final ReactiveCouchbaseTransactionManager managerRx; - final CouchbaseOperations personOperations; - final CouchbaseTransactionManager manager; - - public PersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, - ReactiveCouchbaseTransactionManager mgrRx) { - personOperations = ops; - manager = mgr; - System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); - System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); - System.err.println("manager Manager : " + manager); - - personOperationsRx = opsRx; - managerRx = mgrRx; - System.out - .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); - System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); - System.out.println("managerRx Manager : " + managerRx); - return; - } - - public Mono savePersonErrors(Person person) { - - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx); - return personOperationsRx.insertById(Person.class).one(person) // - . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // - .as(transactionalOperator::transactional); - } - - public Mono savePerson(Person person) { - - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return personOperationsRx.insertById(Person.class).one(person) // - .flatMap(Mono::just) // - .as(transactionalOperator::transactional); - } - - public Mono countDuringTx(Person person) { - - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return personOperationsRx.save(person) // - .then(personOperationsRx.count(new Query(), Person.class)) // - .as(transactionalOperator::transactional); - } - - public Flux saveWithLogs(Person person) { - - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId().toString(), "beforeConvert")), // - personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // - personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // - personOperationsRx.save(person), // - personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // - .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // - .as(transactionalOperator::transactional); - } - - public Flux saveWithErrorLogs(Person person) { - - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId(), "beforeConvert")), // - personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // - personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // - personOperationsRx.save(person), // - personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // - . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // - .as(transactionalOperator::transactional); - } - - @Transactional - public Flux declarativeSavePerson(Person person) { - - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); - } - - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) - public Flux declarativeSavePersonErrors(Person person) { - Person p = personOperations.insertById(Person.class).one(person); - // if(1==1)throw new RuntimeException("poof!"); - Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0); - System.err.println("pp=" + pp); - SimulateFailureException.throwEx(); - return Flux.just(p); - } - } - - /* - @Test - public void deletePersonCBTransactionsRxTmpl() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) - .then(); - })); - result.block(); - Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); - assertNull(pFound, "Should not have found " + pFound); - } - - @Test - public void deletePersonCBTransactionsRxTmplFail() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) - .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) - .then(); - })); - assertThrows(TransactionFailed.class, result::block); - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(pFound, person, "Should have found " + person); - } - - // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// - - @Test - public void deletePersonCBTransactionsRxRepo() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - rxRepo.withCollection(cName).save(person).block(); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); - })); - result.block(); - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should not have found " + pFound); - } - - @Test - public void deletePersonCBTransactionsRxRepoFail() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - rxRepo.withCollection(cName).save(person).block(); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) - .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); - })); - assertThrows(TransactionFailed.class, result::block); - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(pFound, person, "Should have found " + person); - } - - @Test - public void findPersonCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - List docs = new LinkedList<>(); - Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { - docs.add(doc); - return doc; - }).then(); - })); - result.block(); - assertFalse(docs.isEmpty(), "Should have found " + person); - for (Object o : docs) { - assertEquals(o, person, "Should have found " + person); - } - } - - @Test - // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator - // Failed to retrieve PlatformTransactionManager for @Transactional test: - public void insertPersonRbCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - - Mono result = transactions.reactive((ctx) -> { // get the ctx - return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) - . flatMap(it -> Mono.error(new PoofException())).then(); - }); - - try { - result.block(); - } catch (TransactionFailed e) { - e.printStackTrace(); - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should not have found " + pFound); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); - } - - @Test - // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator - // Failed to retrieve PlatformTransactionManager for @Transactional test: - public void replacePersonRbCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - Mono result = transactions.reactive((ctx) -> { // get the ctx - return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) - .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) - .one(pFound.withFirstName("Walt"))) - . flatMap(it -> Mono.error(new PoofException())).then(); - }); - - try { - result.block(); - } catch (TransactionFailed e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); - } - - @Test - public void findPersonSpringTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - List docs = new LinkedList<>(); - Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - Mono result = transactions.reactive((ctx) -> { // get the ctx - return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { - docs.add(doc); - return doc; - }).then(); - }); - result.block(); - assertFalse(docs.isEmpty(), "Should have found " + person); - for (Object o : docs) { - assertEquals(o, person, "Should have found " + person); - } - } - */ - void remove(Collection col, String id) { - remove(col.reactive(), id); - } - - void remove(ReactiveCollection col, String id) { - try { - col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); - } catch (DocumentNotFoundException nfe) { - System.out.println(id + " : " + "DocumentNotFound when deleting"); - } - } - - void remove(CouchbaseTemplate template, String collection, String id) { - remove(template.reactive(), collection, id); - } - - void remove(ReactiveCouchbaseTemplate template, String collection, String id) { - try { - template.removeById(Person.class).inCollection(collection).one(id).block(); - System.out.println("removed " + id); - } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { - System.out.println(id + " : " + "DocumentNotFound when deleting"); - } - } - - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - public TransactionConfig transactionConfig() { - return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); - } - - @Bean - public Cluster couchbaseCluster() { - return Cluster.connect("10.144.220.101", "Administrator", "password"); - } - - /* - @Bean("personService") - PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, - ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { - return new PersonService(ops, mgr, opsRx, mgrRx); - } - */ - - } - - @Data - // @AllArgsConstructor - static class EventLog { - public EventLog() {} - - public EventLog(ObjectId oid, String action) { - this.id = oid.toString(); - this.action = action; - } - - public EventLog(String id, String action) { - this.id = id; - this.action = action; - } - - String id; - String action; - } -} +///* +// * Copyright 2012-2021 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.Cluster; +//import lombok.Data; +//import org.springframework.context.annotation.AnnotationConfigApplicationContext; +//import org.springframework.context.annotation.Bean; +//import org.springframework.data.couchbase.config.BeanNames; +//import org.springframework.data.couchbase.core.CouchbaseOperations; +//import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; +//import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +//import org.springframework.data.domain.Persistable; +//import org.springframework.test.context.transaction.AfterTransaction; +//import org.springframework.test.context.transaction.BeforeTransaction; +//import reactor.core.publisher.Flux; +//import reactor.core.publisher.Mono; +//import reactor.test.StepVerifier; +// +//import java.time.Duration; +//import java.util.ArrayList; +//import java.util.List; +//import java.util.UUID; +// +//import org.junit.jupiter.api.AfterAll; +//import org.junit.jupiter.api.BeforeAll; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.context.support.GenericApplicationContext; +//import org.springframework.dao.DataRetrievalFailureException; +//import org.springframework.data.couchbase.CouchbaseClientFactory; +//import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +//import org.springframework.data.couchbase.core.CouchbaseTemplate; +//import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +//import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +//import org.springframework.data.couchbase.core.query.Query; +//import org.springframework.data.couchbase.domain.Person; +//import org.springframework.data.couchbase.domain.PersonRepository; +//import org.springframework.data.couchbase.domain.ReactivePersonRepository; +//import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +//import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +//import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +//import org.springframework.data.couchbase.util.Capabilities; +//import org.springframework.data.couchbase.util.ClusterType; +//import org.springframework.data.couchbase.util.IgnoreWhen; +//import org.springframework.data.couchbase.util.JavaIntegrationTests; +//import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +//import org.springframework.transaction.annotation.Transactional; +//import org.springframework.transaction.reactive.TransactionalOperator; +//import org.springframework.transaction.support.DefaultTransactionDefinition; +// +//import com.couchbase.client.core.cnc.Event; +//import com.couchbase.client.core.error.DocumentNotFoundException; +//import com.couchbase.client.java.Collection; +//import com.couchbase.client.java.ReactiveCollection; +//import com.couchbase.client.java.kv.RemoveOptions; +// +//import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +//import static org.assertj.core.api.Assertions.assertThat; +// +///** +// * Tests for com.couchbase.transactions without using the spring data transactions framework +// * +// * @author Michael Reiche +// */ +//@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +//@SpringJUnitConfig(CouchbasePersonTransactionReactiveIntegrationTests.Config.class) +////@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +//public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { +// +// @Autowired CouchbaseClientFactory couchbaseClientFactory; +// @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; +// @Autowired CouchbaseSimpleCallbackTransactionManager couchbaseTransactionManager; +// @Autowired ReactivePersonRepository rxRepo; +// @Autowired PersonRepository repo; +// @Autowired ReactiveCouchbaseTemplate rxCBTmpl; +// +// @Autowired Cluster myCluster; +// +// /* DO NOT @Autowired */ PersonService personService; +// +// static GenericApplicationContext context; +// @Autowired ReactiveCouchbaseTemplate operations; +// +// @BeforeAll +// public static void beforeAll() { +// callSuperBeforeAll(new Object() {}); +// context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionReactiveIntegrationTests.Config.class, +// CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); +// } +// +// @AfterAll +// public static void afterAll() { +// callSuperAfterAll(new Object() {}); +// } +// +// @BeforeEach +// public void beforeEachTest() { +// personService = context.getBean(CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); // getting it via autowired results in no @Transactional +// operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); +// operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); +// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); +// operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); +// } +// +// +// @Test // DATAMONGO-2265 +// public void shouldRollbackAfterException() { +// personService.savePersonErrors(new Person(null, "Walter", "White")) // +// .as(StepVerifier::create) // +// .verifyError(RuntimeException.class); +// // operations.findByQuery(Person.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); +// // sleepMs(5000); +// operations.count(new Query(), Person.class) // +// .as(StepVerifier::create) // +// .expectNext(0L) // +// .verifyComplete(); +// } +// +// @Test // DATAMONGO-2265 +// // @Rollback(false) +// public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { +// Person p = new Person(null, "Walter", "White"); +// try { +// personService.declarativeSavePersonErrors(p) // +// .as(StepVerifier::create) // +// .expectComplete(); +// // .verifyError(RuntimeException.class); +// } catch (RuntimeException e) { +// if (e instanceof SimulateFailureException || (e.getMessage() != null && e.getMessage().contains("poof"))) { +// System.err.println(e); +// } else { +// throw e; +// } +// } +// +// } +// +// @Test // DATAMONGO-2265 +// public void commitShouldPersistTxEntries() { +// +// personService.savePerson(new Person(null, "Walter", "White")) // +// .as(StepVerifier::create) // +// .expectNextCount(1) // +// .verifyComplete(); +// +// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count().block(); +// +// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // +// .as(StepVerifier::create) // +// .expectNext(1L) // +// .verifyComplete(); +// } +// +// @Test // DATAMONGO-2265 +// public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { +// +// personService.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // +// .expectNextCount(1) // +// .verifyComplete(); +// +// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // +// .as(StepVerifier::create) // +// .expectNext(1L) // +// .verifyComplete(); +// +// } +// +// @Test // DATAMONGO-2265 +// public void commitShouldPersistTxEntriesAcrossCollections() { +// +// personService.saveWithLogs(new Person(null, "Walter", "White")) // +// .then() // +// .as(StepVerifier::create) // +// .verifyComplete(); +// +// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // +// .as(StepVerifier::create) // +// .expectNext(1L) // +// .verifyComplete(); +// +// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // +// .as(StepVerifier::create) // +// .expectNext(4L) // +// .verifyComplete(); +// } +// +// @Test // DATAMONGO-2265 +// public void rollbackShouldAbortAcrossCollections() { +// +// personService.saveWithErrorLogs(new Person(null, "Walter", "White")) // +// .then() // +// .as(StepVerifier::create) // +// .verifyError(); +// +// operations.count(new Query(), Person.class) // +// .as(StepVerifier::create) // +// .expectNext(0L) // +// .verifyComplete(); +// +// operations.count(new Query(), EventLog.class)// +// .as(StepVerifier::create) // +// .expectNext(0L) // +// .verifyComplete(); +// } +// +// @Test // DATAMONGO-2265 +// public void countShouldWorkInsideTransaction() { +// +// personService.countDuringTx(new Person(null, "Walter", "White")) // +// .as(StepVerifier::create) // +// .expectNext(1L) // +// .verifyComplete(); +// } +// +// @Test // DATAMONGO-2265 +// public void emitMultipleElementsDuringTransaction() { +// +// try { +// personService.saveWithLogs(new Person(null, "Walter", "White")) // +// .as(StepVerifier::create) // +// .expectNextCount(4L) // +// .verifyComplete(); +// } catch (Exception e) { +// System.err.println("Done"); +// throw e; +// } +// } +// +// @Test // DATAMONGO-2265 +// public void errorAfterTxShouldNotAffectPreviousStep() { +// +// Person p = new Person(1, "Walter", "White"); +// remove(couchbaseTemplate, "_default", p.getId().toString()); +// personService.savePerson(p) // +// //.delayElement(Duration.ofMillis(100)) // +// .then(Mono.error(new RuntimeException("my big bad evil error"))).as(StepVerifier::create) // +// .expectError() +// .verify(); +// //.expectError() // +// //.as(StepVerifier::create) +// //.expectNext(p) +// //.verifyComplete(); +// +// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // +// .as(StepVerifier::create) // +// .expectNext(1L) // +// .verifyComplete(); +// } +// +// // @RequiredArgsConstructor +// static class PersonService { +// +// final ReactiveCouchbaseOperations personOperationsRx; +// final ReactiveCouchbaseTransactionManager managerRx; +// final CouchbaseOperations personOperations; +// final CouchbaseCallbackTransactionManager manager; +// +// public PersonService(CouchbaseOperations ops, CouchbaseCallbackTransactionManager mgr, ReactiveCouchbaseOperations opsRx, +// ReactiveCouchbaseTransactionManager mgrRx) { +// personOperations = ops; +// manager = mgr; +// System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); +//// System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); +// System.err.println("manager Manager : " + manager); +// +// personOperationsRx = opsRx; +// managerRx = mgrRx; +// System.out +// .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); +// System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); +// System.out.println("managerRx Manager : " + managerRx); +// return; +// } +// +// public Mono savePersonErrors(Person person) { +// +// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx); +// return personOperationsRx.insertById(Person.class).one(person) // +// . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // +// .as(transactionalOperator::transactional); +// } +// +// public Mono savePerson(Person person) { +// +// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, +// new DefaultTransactionDefinition()); +// +// return personOperationsRx.insertById(Person.class).one(person) // +// .flatMap(Mono::just) // +// .as(transactionalOperator::transactional); +// } +// +// public Mono countDuringTx(Person person) { +// +// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, +// new DefaultTransactionDefinition()); +// +// return personOperationsRx.save(person) // +// .then(personOperationsRx.count(new Query(), Person.class)) // +// .as(transactionalOperator::transactional); +// } +// +// public Flux saveWithLogs(Person person) { +// +// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, +// new DefaultTransactionDefinition()); +// +// return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId().toString(), "beforeConvert")), // +// personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // +// personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // +// personOperationsRx.save(person), // +// personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // +// .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // +// .as(transactionalOperator::transactional); +// } +// +// public Flux saveWithErrorLogs(Person person) { +// +// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, +// new DefaultTransactionDefinition()); +// +// return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId(), "beforeConvert")), // +// personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // +// personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // +// personOperationsRx.save(person), // +// personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // +// . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // +// .as(transactionalOperator::transactional); +// } +// +// @Transactional +// public Flux declarativeSavePerson(Person person) { +// +// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, +// new DefaultTransactionDefinition()); +// +// return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); +// } +// +// @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +// public Flux declarativeSavePersonErrors(Person person) { +// Person p = personOperations.insertById(Person.class).one(person); +// // if(1==1)throw new RuntimeException("poof!"); +// Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0); +// System.err.println("pp=" + pp); +// SimulateFailureException.throwEx(); +// return Flux.just(p); +// } +// } +// +// /* +// @Test +// public void deletePersonCBTransactionsRxTmpl() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); +// +// Mono result = transactions.reactive(((ctx) -> { // get the ctx +// return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) +// .then(); +// })); +// result.block(); +// Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); +// assertNull(pFound, "Should not have found " + pFound); +// } +// +// @Test +// public void deletePersonCBTransactionsRxTmplFail() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// cbTmpl.insertById(Person.class).inCollection(cName).one(person); +// +// Mono result = transactions.reactive(((ctx) -> { // get the ctx +// return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) +// .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) +// .then(); +// })); +// assertThrows(TransactionFailedException.class, result::block); +// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); +// assertEquals(pFound, person, "Should have found " + person); +// } +// +// // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// +// +// @Test +// public void deletePersonCBTransactionsRxRepo() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// rxRepo.withCollection(cName).save(person).block(); +// +// Mono result = transactions.reactive(((ctx) -> { // get the ctx +// return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); +// })); +// result.block(); +// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); +// assertNull(pFound, "Should not have found " + pFound); +// } +// +// @Test +// public void deletePersonCBTransactionsRxRepoFail() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// rxRepo.withCollection(cName).save(person).block(); +// +// Mono result = transactions.reactive(((ctx) -> { // get the ctx +// return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) +// .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); +// })); +// assertThrows(TransactionFailedException.class, result::block); +// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); +// assertEquals(pFound, person, "Should have found " + person); +// } +// +// @Test +// public void findPersonCBTransactions() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// cbTmpl.insertById(Person.class).inCollection(cName).one(person); +// List docs = new LinkedList<>(); +// Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); +// Mono result = transactions.reactive(((ctx) -> { // get the ctx +// return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { +// docs.add(doc); +// return doc; +// }).then(); +// })); +// result.block(); +// assertFalse(docs.isEmpty(), "Should have found " + person); +// for (Object o : docs) { +// assertEquals(o, person, "Should have found " + person); +// } +// } +// +// @Test +// // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator +// // Failed to retrieve PlatformTransactionManager for @Transactional test: +// public void insertPersonRbCBTransactions() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// +// Mono result = transactions.reactive((ctx) -> { // get the ctx +// return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) +// . flatMap(it -> Mono.error(new PoofException())).then(); +// }); +// +// try { +// result.block(); +// } catch (TransactionFailedException e) { +// e.printStackTrace(); +// if (e.getCause() instanceof PoofException) { +// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); +// assertNull(pFound, "Should not have found " + pFound); +// return; +// } else { +// e.printStackTrace(); +// } +// } +// throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); +// } +// +// @Test +// // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator +// // Failed to retrieve PlatformTransactionManager for @Transactional test: +// public void replacePersonRbCBTransactions() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// cbTmpl.insertById(Person.class).inCollection(cName).one(person); +// Mono result = transactions.reactive((ctx) -> { // get the ctx +// return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) +// .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) +// .one(pFound.withFirstName("Walt"))) +// . flatMap(it -> Mono.error(new PoofException())).then(); +// }); +// +// try { +// result.block(); +// } catch (TransactionFailedException e) { +// if (e.getCause() instanceof PoofException) { +// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); +// assertEquals(person, pFound, "Should have found " + person); +// return; +// } else { +// e.printStackTrace(); +// } +// } +// throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); +// } +// +// @Test +// public void findPersonSpringTransactions() { +// Person person = new Person(1, "Walter", "White"); +// remove(cbTmpl, cName, person.getId().toString()); +// cbTmpl.insertById(Person.class).inCollection(cName).one(person); +// List docs = new LinkedList<>(); +// Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); +// Mono result = transactions.reactive((ctx) -> { // get the ctx +// return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { +// docs.add(doc); +// return doc; +// }).then(); +// }); +// result.block(); +// assertFalse(docs.isEmpty(), "Should have found " + person); +// for (Object o : docs) { +// assertEquals(o, person, "Should have found " + person); +// } +// } +// */ +// void remove(Collection col, String id) { +// remove(col.reactive(), id); +// } +// +// void remove(ReactiveCollection col, String id) { +// try { +// col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); +// } catch (DocumentNotFoundException nfe) { +// System.out.println(id + " : " + "DocumentNotFound when deleting"); +// } +// } +// +// void remove(CouchbaseTemplate template, String collection, String id) { +// remove(template.reactive(), collection, id); +// } +// +// void remove(ReactiveCouchbaseTemplate template, String collection, String id) { +// try { +// template.removeById(Person.class).inCollection(collection).one(id).block(); +// System.out.println("removed " + id); +// } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { +// System.out.println(id + " : " + "DocumentNotFound when deleting"); +// } +// } +// +// @Configuration +// @EnableCouchbaseRepositories("org.springframework.data.couchbase") +// @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") +// static class Config extends AbstractCouchbaseConfiguration { +// +// @Override +// public String getConnectionString() { +// return connectionString(); +// } +// +// @Override +// public String getUserName() { +// return config().adminUsername(); +// } +// +// @Override +// public String getPassword() { +// return config().adminPassword(); +// } +// +// @Override +// public String getBucketName() { +// return bucketName(); +// } +// +// @Bean +// public Cluster couchbaseCluster() { +// return Cluster.connect("10.144.220.101", "Administrator", "password"); +// } +// +// /* +// @Bean("personService") +// PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, +// ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { +// return new PersonService(ops, mgr, opsRx, mgrRx); +// } +// */ +// +// } +// +// @Data +// // @AllArgsConstructor +// static class EventLog { +// public EventLog() {} +// +// public EventLog(ObjectId oid, String action) { +// this.id = oid.toString(); +// this.action = action; +// } +// +// public EventLog(String id, String action) { +// this.id = id; +// this.action = action; +// } +// +// String id; +// String action; +// } +//} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java index 7ca0847d1..3c6ddfd36 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -19,6 +19,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.error.TransactionFailedException; import org.junit.jupiter.api.Disabled; import reactor.core.publisher.Mono; @@ -55,11 +57,6 @@ import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.RemoveOptions; -import com.couchbase.transactions.TransactionDurabilityLevel; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; -import com.couchbase.transactions.error.TransactionFailed; /** * Tests for com.couchbase.transactions without using the spring data transactions framework @@ -115,7 +112,7 @@ public void replacePersonTemplate() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof SimulateFailureException) { Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertEquals(person, pFound, "Should have found " + person); @@ -125,7 +122,7 @@ public void replacePersonTemplate() { } Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -142,7 +139,7 @@ public void replacePersonRbTemplate() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof SimulateFailureException) { Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertEquals(person, pFound, "Should have found " + person); @@ -152,7 +149,7 @@ public void replacePersonRbTemplate() { } // Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -168,7 +165,7 @@ public void insertPersonTemplate() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof SimulateFailureException) { Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertNull(pFound, "Should NOT have found " + pFound); @@ -178,7 +175,7 @@ public void insertPersonTemplate() { } Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -193,7 +190,7 @@ public void insertPersonRbTemplate() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof SimulateFailureException) { Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertNull(pFound, "Should NOT have found " + pFound); @@ -203,7 +200,7 @@ public void insertPersonRbTemplate() { } // Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -220,7 +217,7 @@ public void replacePersonRbRepo() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof SimulateFailureException) { Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); assertEquals(person, pFound, "Should have found " + person); @@ -230,7 +227,7 @@ public void replacePersonRbRepo() { } // Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -246,7 +243,7 @@ public void insertPersonRbRepo() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof SimulateFailureException) { Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); assertNull(pFound, "Should NOT have found " + pFound); @@ -256,7 +253,7 @@ public void insertPersonRbRepo() { } // Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -273,7 +270,7 @@ public void insertPersonRepo() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof SimulateFailureException) { Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); assertNull(pFound, "Should NOT have found " + pFound); @@ -283,7 +280,7 @@ public void insertPersonRepo() { } Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } /* @@ -312,7 +309,7 @@ public void replacePersonRbSpringTransactional() { try { result.block(); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertEquals(person, pFound, "Should have found " + person); @@ -322,7 +319,7 @@ public void replacePersonRbSpringTransactional() { } Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } */ @Test @@ -427,13 +424,6 @@ public String getPassword() { public String getBucketName() { return bucketName(); } - - @Override - public TransactionConfig transactionConfig() { - return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); - } - } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java index 547959cec..7a6fcf809 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java @@ -53,9 +53,6 @@ import org.springframework.transaction.annotation.Transactional; import com.couchbase.client.core.cnc.Event; -import com.couchbase.transactions.TransactionDurabilityLevel; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; import com.example.demo.CouchbaseTransactionManager; import com.example.demo.CouchbaseTransactionalTemplate; @@ -98,16 +95,9 @@ public String getBucketName() { return bucketName(); } - @Override - public TransactionConfig transactionConfig() { - return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.NONE).build(); - } - @Bean - public CouchbaseTransactionManager transactionManager(@Autowired CouchbaseTemplate template, - @Autowired TransactionConfig transactionConfig) { - return new CouchbaseTransactionManager(template, transactionConfig); + public CouchbaseTransactionManager transactionManager(@Autowired CouchbaseTemplate template) { + return new CouchbaseTransactionManager(template); } @Bean diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java index 32786ee67..ffd19e851 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java @@ -55,9 +55,6 @@ import org.springframework.transaction.annotation.Transactional; import com.couchbase.client.core.cnc.Event; -import com.couchbase.transactions.TransactionDurabilityLevel; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; /** * @author Christoph Strobl @@ -96,13 +93,6 @@ public String getPassword() { public String getBucketName() { return bucketName(); } - - @Override - public TransactionConfig transactionConfig() { - return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.NONE).build(); - } - } @Autowired CouchbaseTemplate template; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save index b6f263b54..894cf8594 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save @@ -56,7 +56,7 @@ import com.couchbase.transactions.TransactionDurabilityLevel; import com.couchbase.transactions.TransactionResult; import com.couchbase.transactions.config.TransactionConfig; import com.couchbase.transactions.config.TransactionConfigBuilder; -import com.couchbase.transactions.error.TransactionFailed; +import com.couchbase.transactions.error.TransactionFailedException; /** * Tests for com.couchbase.transactions without using the spring data transactions framework @@ -112,7 +112,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { return null; }); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals(person, pFound, "Should have found " + person); @@ -122,7 +122,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { } Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -138,7 +138,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { .flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) .flatMap(it -> Mono.error(new PoofException())).then()); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals(person, pFound, "Should have found " + person); @@ -148,7 +148,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { } // Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -164,7 +164,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { // .flatMap(it -> Mono.error(new PoofException())) .then()); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertNull(pFound, "Should NOT have found " + pFound); @@ -174,7 +174,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { } Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -189,7 +189,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { .one(person).flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) .flatMap(it -> Mono.error(new PoofException())).then()); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertNull(pFound, "Should NOT have found " + pFound); @@ -199,7 +199,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { } // Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -215,7 +215,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) .flatMap(it -> Mono.error(new PoofException())).then()); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals(person, pFound, "Should have found " + person); @@ -225,7 +225,7 @@ public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { } // Person pFound = repo.withCollection(cName).findById(person.getId().toString()); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -239,7 +239,7 @@ try { .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) // replace .flatMap(it -> Mono.error(new PoofException())).then()); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertNull(pFound, "Should NOT have found " + pFound); @@ -249,7 +249,7 @@ try { } // Person pFound = repo.withCollection(cName).findById(person.getId().toString()); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -265,7 +265,7 @@ try { .then()); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertNull(pFound, "Should NOT have found " + pFound); @@ -275,7 +275,7 @@ try { } Person pFound = repo.withCollection(cName).findById(person.getId().toString()); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } @Test @@ -293,7 +293,7 @@ try { .as(txOperator::transactional).then(); }, false); - } catch (TransactionFailed e) { + } catch (TransactionFailedException e) { if (e.getCause() instanceof PoofException) { Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals(person, pFound, "Should have found " + person); @@ -303,7 +303,7 @@ try { } Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailed exception with a cause of PoofException"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); } void remove(Collection col, String id) { diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index a7dd9416e..5deab4ba9 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -31,6 +31,8 @@ import com.couchbase.client.core.service.Service; import com.couchbase.client.java.ClusterOptions; import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -62,7 +64,10 @@ public abstract class ClusterAwareIntegrationTests { @BeforeAll static void setup(TestClusterConfig config) { testClusterConfig = config; - ClusterEnvironment env = ClusterEnvironment.builder().build(); + // todo gp disabling cleanupLostAttempts to simplify output during development + ClusterEnvironment env = ClusterEnvironment.builder() + .transactionsConfig(TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false))) + .build(); String connectString = connectionString(); try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectString, authenticator(), bucketName(), null, env)) { From 4e93e556c44d10b9fe2771337b02d08700ed0955 Mon Sep 17 00:00:00 2001 From: Michael Reiche Date: Tue, 3 May 2022 11:24:56 -0700 Subject: [PATCH 6/9] Commit before pulling Graham's changes. --- pom.xml | 3 + .../AttemptContextReactiveAccessor.java | 104 +++--- .../transactions/TransactionsReactive.java | 2 +- .../ReactiveCouchbaseClientFactory.java | 10 +- .../AbstractCouchbaseConfiguration.java | 34 ++ .../ReactiveReplaceByIdOperationSupport.java | 1 - .../CrudMethodMetadataPostProcessor.java | 4 +- .../SimpleReactiveCouchbaseRepository.java | 36 +- .../support/TransactionResultHolder.java | 4 +- .../ReactiveCouchbaseClientUtils.java | 8 +- .../transaction/TransactionsWrapper.java | 2 +- .../CouchbaseTransactionInterceptor.java | 38 +- .../domain/ReactiveAirportRepository.java | 7 + ...chbaseRepositoryQueryIntegrationTests.java | 22 ++ .../data/couchbase/transactions/Config.java | 71 ++++ ...basePersonTransactionIntegrationTests.java | 330 +----------------- ...onTransactionReactiveIntegrationTests.java | 50 ++- .../couchbase/transactions/PersonService.java | 292 ++++++++++++++++ .../transactions/TransactionInterception.save | 51 +++ .../couchbase/util/JavaIntegrationTests.java | 6 +- 20 files changed, 636 insertions(+), 439 deletions(-) create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/Config.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/PersonService.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/TransactionInterception.save diff --git a/pom.xml b/pom.xml index 99a817e2b..87ed1eda6 100644 --- a/pom.xml +++ b/pom.xml @@ -292,10 +292,12 @@ false + @@ -327,6 +329,7 @@ org.apache.maven.plugins maven-failsafe-plugin + false **/*IntegrationTest.java **/*IntegrationTests.java diff --git a/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java index f59d110ed..5fdb75b2a 100644 --- a/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java +++ b/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java @@ -16,6 +16,11 @@ */ package com.couchbase.transactions; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; + import com.couchbase.client.core.annotation.Stability; import com.couchbase.transactions.config.MergedTransactionConfig; import com.couchbase.transactions.config.PerTransactionConfig; @@ -24,11 +29,6 @@ import com.couchbase.transactions.forwards.Supported; import com.couchbase.transactions.log.TransactionLogger; -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.Optional; -import java.util.UUID; - /** * To access the AttemptContextReactive held by AttemptContext * @@ -36,55 +36,61 @@ */ public class AttemptContextReactiveAccessor { - public static AttemptContextReactive getACR(AttemptContext attemptContext) { - return attemptContext.ctx(); - } + public static AttemptContextReactive getACR(AttemptContext attemptContext) { + return attemptContext.ctx(); + } + + public static AttemptContext from(AttemptContextReactive attemptContextReactive) { + return new AttemptContext(attemptContextReactive); + } - public static AttemptContext from(AttemptContextReactive attemptContextReactive) { - return new AttemptContext(attemptContextReactive); - } + public static TransactionLogger getLogger(AttemptContextReactive attemptContextReactive) { + return attemptContextReactive.LOGGER; + } - public static TransactionLogger getLogger(AttemptContextReactive attemptContextReactive){ - return attemptContextReactive.LOGGER; - } - @Stability.Internal - public static AttemptContextReactive newAttemptContextReactive(TransactionsReactive transactions){ - PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); - MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); + @Stability.Internal + public static AttemptContextReactive newAttemptContextReactive(TransactionsReactive transactions) { + PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); + MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); - TransactionContext overall = new TransactionContext( - transactions.cleanup().clusterData().cluster().environment().requestTracer(), - transactions.cleanup().clusterData().cluster().environment().eventBus(), - UUID.randomUUID().toString(), now(), Duration.ZERO, merged); + TransactionContext overall = new TransactionContext( + transactions.cleanup().clusterData().cluster().environment().requestTracer(), + transactions.cleanup().clusterData().cluster().environment().eventBus(), UUID.randomUUID().toString(), now(), + Duration.ZERO, merged); - String txnId = UUID.randomUUID().toString(); - overall.LOGGER.info(configDebug(transactions.config(), perConfig)); - return transactions.createAttemptContext(overall, merged, txnId); - } + String txnId = UUID.randomUUID().toString(); + overall.LOGGER.info(configDebug(transactions.config(), perConfig)); + return newAttemptContextReactive(transactions, overall, merged, txnId); + } - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); - } + @Stability.Internal + public static AttemptContextReactive newAttemptContextReactive(TransactionsReactive reactive, + TransactionContext overall, MergedTransactionConfig merged, String txnId) { + return reactive.createAttemptContext(overall, merged, txnId); + } - static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { - StringBuilder sb = new StringBuilder(); - sb.append("library version: "); - sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); - sb.append(" config: "); - sb.append("atrs="); - sb.append(config.numAtrs()); - sb.append(", metadataCollection="); - sb.append(config.metadataCollection()); - sb.append(", expiry="); - sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); - sb.append("msecs durability="); - sb.append(config.durabilityLevel()); - sb.append(" per-txn config="); - sb.append(" durability="); - sb.append(perConfig.durabilityLevel()); - sb.append(", supported="); - sb.append(Supported.SUPPORTED); - return sb.toString(); - } + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { + StringBuilder sb = new StringBuilder(); + sb.append("library version: "); + sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); + sb.append(" config: "); + sb.append("atrs="); + sb.append(config.numAtrs()); + sb.append(", metadataCollection="); + sb.append(config.metadataCollection()); + sb.append(", expiry="); + sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); + sb.append("msecs durability="); + sb.append(config.durabilityLevel()); + sb.append(" per-txn config="); + sb.append(" durability="); + sb.append(perConfig.durabilityLevel()); + sb.append(", supported="); + sb.append(Supported.SUPPORTED); + return sb.toString(); + } } diff --git a/src/main/java/com/couchbase/transactions/TransactionsReactive.java b/src/main/java/com/couchbase/transactions/TransactionsReactive.java index 1e64e803e..21180b3c2 100644 --- a/src/main/java/com/couchbase/transactions/TransactionsReactive.java +++ b/src/main/java/com/couchbase/transactions/TransactionsReactive.java @@ -364,7 +364,7 @@ private Mono executeImplicitCommit(AttemptContextReactiv } // TODO: changed from package-protected to public (could have just used an accessor class in same package) - public AttemptContextReactive createAttemptContext(TransactionContext overall, + AttemptContextReactive createAttemptContext(TransactionContext overall, MergedTransactionConfig config, String attemptId) { // null only happens in testing with Mockito, harmless diff --git a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java index cfeab6cef..bbaeabf3e 100644 --- a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java @@ -46,8 +46,6 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* /** * Provides access to the managed SDK {@link Cluster} reference. */ - //Cluster getCluster(); - Mono getCluster(); /** @@ -58,8 +56,6 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* /** * Provides access to the managed SDK {@link Scope} reference. */ - //Scope getScope(); - Mono getScope(); /** @@ -67,9 +63,8 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* * * @param name the name of the collection. If null is passed in, the default collection is assumed. */ - //Collection getCollection(String name); - Mono getCollection(String name); + /** * Provides access to the default collection. */ @@ -113,7 +108,4 @@ ClientSession getSession(ClientSessionOptions options, Transactions transactions */ boolean isTransactionActive(); - //CouchbaseStuffHandle getTransactionalOperator(); - - //ReactiveCouchbaseClientFactory with(CouchbaseStuffHandle txOp); } diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 127044c1d..e6b019675 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -30,6 +30,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -49,9 +50,15 @@ import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.interceptor.CouchbaseTransactionInterceptor; import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -436,4 +443,31 @@ public QueryScanConsistency getDefaultConsistency() { return null; } + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) { + TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(); + interceptor.setTransactionAttributeSource(transactionAttributeSource); + return interceptor; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionAttributeSource transactionAttributeSource() { + return new AnnotationTransactionAttributeSource(); + } + + @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( + TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { + + BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); + advisor.setTransactionAttributeSource(transactionAttributeSource); + advisor.setAdvice(transactionInterceptor); + // if (this.enableTx != null) { + // advisor.setOrder(this.enableTx.getNumber("order")); + // } + return advisor; + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 242cb3f9d..beda245b5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -128,7 +128,6 @@ public Mono one(T object) { CouchbaseDocument converted = support.encodeEntity(object).block(); reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { if (s == null || s.getAttemptContextReactive() == null) { - System.err.println("ReactiveReplaceById: not"); Mono op = template.getCouchbaseClientFactory() .withScope(pArgs.getScope()).getCollection(pArgs.getCollection()); return op.flatMap(collection -> collection.reactive() diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java index 0e5dd19bf..e37636b70 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java @@ -168,10 +168,10 @@ public Object invoke(MethodInvocation invocation) throws Throwable { try { return invocation.proceed(); } finally { - // TransactionSynchronizationManager.unbindResource(method); + TransactionSynchronizationManager.unbindResource(method); } } finally { - // currentInvocation.set(oldInvocation); + currentInvocation.set(oldInvocation); } } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 90a485c45..e4fc36cdb 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.repository.support; +import com.couchbase.client.java.CommonOptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -74,6 +75,24 @@ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation e @SuppressWarnings("unchecked") @Override public Mono save(S entity) { + return save(entity, getScope(), getCollection()); + } + + @Override + public Flux findAll(Sort sort) { + return findAll(new Query().with(sort)); + } + + @Override + public Flux saveAll(Iterable entities) { + Assert.notNull(entities, "The given Iterable of entities must not be null!"); + String scopeName = getScope(); + String collection = getCollection(); + return Flux.fromIterable(entities).flatMap(e -> save(e, scopeName, collection)); + } + + @SuppressWarnings("unchecked") + public Mono save(S entity, String scopeName, String collectionName) { Assert.notNull(entity, "Entity must not be null!"); Mono result; final CouchbasePersistentEntity mapperEntity = operations.getConverter().getMappingContext() @@ -86,31 +105,20 @@ public Mono save(S entity) { if (!versionPresent) { // the entity doesn't have a version property // No version field - no cas - result = (Mono) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + result = (Mono) operations.upsertById(getJavaType()).inScope(scopeName).inCollection(collectionName) .one(entity); } else if (existingDocument) { // there is a version property, and it is non-zero // Updating existing document with cas - result = (Mono) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + result = (Mono) operations.replaceById(getJavaType()).inScope(scopeName).inCollection(collectionName) .one(entity); } else { // there is a version property, but it's zero or not set. // Creating new document - result = (Mono) operations.insertById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + result = (Mono) operations.insertById(getJavaType()).inScope(scopeName).inCollection(collectionName) .one(entity); } return result; } - @Override - public Flux findAll(Sort sort) { - return findAll(new Query().with(sort)); - } - - @Override - public Flux saveAll(Iterable entities) { - Assert.notNull(entities, "The given Iterable of entities must not be null!"); - return Flux.fromIterable(entities).flatMap(e -> save(e)); - } - @Override public Flux saveAll(Publisher entityStream) { Assert.notNull(entityStream, "The given Iterable of entities must not be null!"); diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java index c3282a489..a3f5ed84f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java @@ -30,7 +30,7 @@ public class TransactionResultHolder { SingleQueryTransactionResult singleQueryResult; public TransactionResultHolder(TransactionGetResult getResult) { - // we don't need the content and we don't have access to the transcoder an txnMeta (and we don't need them either). + // we don't need the content and we don't have access to the transcoder and txnMeta (and we don't need them either). this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); this.singleQueryResult = null; @@ -46,6 +46,6 @@ public TransactionGetResult transactionGetResult() { } public SingleQueryTransactionResult singleQueryResult() { - return singleQueryResult(); + return singleQueryResult; } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java index e554352d9..8f8a60323 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java @@ -138,18 +138,16 @@ private static Mono doGetCouchbaseTemplate(@Nullable //CouchbaseResourceHolder h = (CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager // .getResource(factory); - TransactionSynchronizationManager.forCurrentTransaction() - .flatMap((synchronizationManager) -> { System.out.println(synchronizationManager.getResource(factory)); return null; }); return TransactionSynchronizationManager.forCurrentTransaction() - .flatMap(x -> { System.err.println("forCurrentTransaction: getResource() : "+x.getResource(factory.getCluster().block())); return Mono.just(x);}) + .flatMap(x -> {/* System.err.println("forCurrentTransaction: getResource() : "+x.getResource(factory.getCluster().block()));*/ return Mono.just(x);}) .filter(TransactionSynchronizationManager::isSynchronizationActive) // .flatMap(synchronizationManager -> { return doGetSession(synchronizationManager, factory, sessionSynchronization) // .flatMap(it -> getCouchbaseTemplateOrDefault(dbName, factory.withSession(it), converter)); // rx TxMgr }) // .onErrorResume(NoTransactionException.class, - e -> { System.err.println("noCurrentTransaction: "); return getCouchbaseTemplateOrDefault(dbName, + e -> { /* System.err.println("noCurrentTransaction: "); */return getCouchbaseTemplateOrDefault(dbName, getNonReactiveSession(factory) != null ? factory.withSession(getNonReactiveSession(factory)) : factory, converter);}) // blocking TxMgr .switchIfEmpty(getCouchbaseTemplateOrDefault(dbName, factory, converter)); @@ -162,7 +160,7 @@ private static ClientSession getNonReactiveSession(ReactiveCouchbaseClientFactor h = ((CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager .getResource(factory));// MN's CouchbaseTransactionManager } - System.err.println("getNonreactiveSession: "+ h); + //System.err.println("getNonreactiveSession: "+ h); return h != null ? h.getSession() : null; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java index e3db6bd1a..6976f04da 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java @@ -67,7 +67,7 @@ public Mono run(Function> Mono ob = Mono.fromCallable(() -> { String txnId = UUID.randomUUID().toString(); overall.LOGGER.info(configDebug(config, perConfig)); - return transactions.reactive().createAttemptContext(overall, merged, txnId); + return AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive(),overall, merged, txnId); }).flatMap(ctx -> { AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", diff --git a/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java index 26231197e..66b1c00e2 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java @@ -206,13 +206,47 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class targe } PlatformTransactionManager ptm = asPlatformTransactionManager(tm); + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { - return super.invokeWithinTransaction(method, targetClass, invocation); + //return super.invokeWithinTransaction(method, targetClass, invocation); + // Standard transaction demarcation with getTransaction and commit/rollback calls. + + Object retVal = null; + boolean success=false; + do { + TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); + try { + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + retVal = invocation.proceedWithInvocation(); + success = true; + } catch (Throwable ex) { + // target invocation exception + completeTransactionAfterThrowing(txInfo, + ex); + //throw ex; + } finally { + cleanupTransactionInfo(txInfo); + } + if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { + // Set rollback-only in case of Vavr failure matching our rollback rules... + TransactionStatus status = txInfo.getTransactionStatus(); + if (status != null && txAttr != null) { + retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); + } + } + + if( retVal != null) { + // this could go directly after succeed = true except for the cleanupTransactionInfo(txInfo) and the vavrPresent + commitTransactionAfterReturning(txInfo); + } + } while (!success); + + return retVal; } else { - final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); Object result; final ThrowableHolder throwableHolder = new ThrowableHolder(); diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index 59426ec27..90b397cd9 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.domain; +import lombok.val; +import org.springframework.data.couchbase.core.query.WithConsistency; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,6 +46,11 @@ public interface ReactiveAirportRepository extends ReactiveCouchbaseRepository, DynamicProxyable { + + @Query("SELECT META(#{#n1ql.bucket}).id AS __id, META(#{#n1ql.bucket}).cas AS __cas, meta().id as id FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} #{[1]}") + @ScanConsistency(query=QueryScanConsistency.NOT_BOUNDED) + Flux findIdByDynamicN1ql(String docType, String queryStatement); + @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Flux findAll(); diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index cc3bd5f0d..f80a37204 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -92,6 +92,28 @@ void shouldSaveAndFindAll() { } } + @Test + void testQuery() { + Airport vie = null; + Airport jfk = null; + try { + vie = new Airport("airports::vie", "vie", "low1"); + airportRepository.save(vie).block(); + jfk = new Airport("airports::jfk", "JFK", "xxxx"); + airportRepository.save(jfk).block(); + + List all = airportRepository.findIdByDynamicN1ql("","").toStream().collect(Collectors.toList()); + System.out.println(all); + assertFalse(all.isEmpty()); + assertTrue(all.stream().anyMatch(a -> a.equals("airports::vie"))); + assertTrue(all.stream().anyMatch(a -> a.equals("airports::jfk"))); + + } finally { + airportRepository.delete(vie).block(); + airportRepository.delete(jfk).block(); + } + } + @Test void findBySimpleProperty() { Airport vie = null; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/Config.java b/src/test/java/org/springframework/data/couchbase/transactions/Config.java new file mode 100644 index 000000000..a2e21c80d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/Config.java @@ -0,0 +1,71 @@ +package org.springframework.data.couchbase.transactions; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.transactions.TransactionDurabilityLevel; +import com.couchbase.transactions.config.TransactionConfig; +import com.couchbase.transactions.config.TransactionConfigBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; + +@Configuration +@EnableCouchbaseRepositories("org.springframework.data.couchbase") +@EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") +@EnableTransactionManagement +class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return ClusterAwareIntegrationTests.connectionString(); + } + + @Override + public String getUserName() { + return ClusterAwareIntegrationTests.config() + .adminUsername(); + } + + @Override + public String getPassword() { + return ClusterAwareIntegrationTests.config() + .adminPassword(); + } + + @Override + public String getBucketName() { + return ClusterAwareIntegrationTests.bucketName(); + } + + @Override + public TransactionConfig transactionConfig() { + // expirationTime 20 minutes for stepping with the debugger + return TransactionConfigBuilder.create() + .logDirectly(Event.Severity.INFO) + .logOnFailure(true, + Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(20)) + .durabilityLevel(TransactionDurabilityLevel.MAJORITY) + .build(); + } + + /* + beforeAll creates a PersonService bean in the applicationContext + + context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionIntegrationTests.Config.class, + PersonService.class); + + @Bean("personService") + PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + return new PersonService(ops, mgr, opsRx, mgrRx); + } + */ + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index 7ee1c3862..6f2be2353 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import lombok.Data; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Duration; @@ -38,68 +37,41 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; import org.springframework.context.support.GenericApplicationContext; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -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.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; -import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.couchbase.transaction.ClientSessionOptions; -import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.TransactionsWrapper; -import org.springframework.data.couchbase.transaction.interceptor.CouchbaseTransactionInterceptor; import org.springframework.data.couchbase.util.Capabilities; 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.ReactiveTransaction; import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.config.TransactionManagementConfigUtils; -import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; -import org.springframework.transaction.interceptor.TransactionAttributeSource; -import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.transaction.reactive.TransactionContextManager; import org.springframework.transaction.reactive.TransactionSynchronizationManager; -import org.springframework.transaction.reactive.TransactionalOperator; -import org.springframework.transaction.support.DefaultTransactionDefinition; -import com.couchbase.client.core.cnc.Event; import com.couchbase.client.core.error.DocumentNotFoundException; import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.RemoveOptions; -import com.couchbase.transactions.TransactionDurabilityLevel; import com.couchbase.transactions.TransactionResult; import com.couchbase.transactions.Transactions; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; import com.couchbase.transactions.error.TransactionFailed; import com.couchbase.transactions.error.external.TransactionOperationFailed; @@ -112,7 +84,7 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(CouchbasePersonTransactionIntegrationTests.Config.class) +@SpringJUnitConfig(classes = { Config.class, PersonService.class }) public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -124,7 +96,7 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT @Autowired CouchbaseTemplate cbTmpl; @Autowired ReactiveCouchbaseTemplate rxCBTmpl; @Autowired Transactions transactions; - /* DO NOT @Autowired - it will result in no @Transactional annotation behavior */ PersonService personService; + @Autowired PersonService personService; @Autowired CouchbaseTemplate operations; static GenericApplicationContext context; @@ -132,19 +104,21 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT @BeforeAll public static void beforeAll() { callSuperBeforeAll(new Object() {}); - context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionIntegrationTests.Config.class, - PersonService.class, CouchbasePersonTransactionIntegrationTests.TransactionInterception.class); + context = new AnnotationConfigApplicationContext(Config.class, + PersonService.class /*, TransactionInterception.class*/); } @AfterAll public static void afterAll() { callSuperAfterAll(new Object() {}); - context.close(); + if (context != null) { + context.close(); + } } @BeforeEach public void beforeEachTest() { - personService = context.getBean(PersonService.class); // getting it via autowired results in no @Transactional + //personService = context.getBean(PersonService.class); // getting it via autowired results in no @Transactional // Skip this as we just one to track TransactionContext operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); // doesn't work??? operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); @@ -810,53 +784,6 @@ void assertThrowsCause(Class exceptionClass, Class causeClass, FunctiongetNumber("order")); - // } - return advisor; - } - - } - - @Service - @Component - @EnableTransactionManagement - static - // @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) - class PersonService { - - final CouchbaseOperations personOperations; - final CouchbaseTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; - final ReactiveCouchbaseOperations personOperationsRx; - final ReactiveCouchbaseTransactionManager managerRx; - - public PersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, - ReactiveCouchbaseTransactionManager mgrRx) { - personOperations = ops; - manager = mgr; - System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); - System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); - System.err.println("manager Manager : " + manager); - - personOperationsRx = opsRx; - managerRx = mgrRx; - System.out - .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); - System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); - System.out.println("managerRx Manager : " + managerRx); - return; - } - - public Person savePersonErrors(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return personOperationsRx.insertById(Person.class).one(person)// - . flatMap(it -> Mono.error(new SimulateFailureException()))// - .as(transactionalOperator::transactional).block(); - } - - public Person savePerson(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - return personOperationsRx.insertById(Person.class).one(person)// - .as(transactionalOperator::transactional).block(); - } - - public Long countDuringTx(Person person) { - assertInAnnotationTransaction(false); - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return personOperationsRx.insertById(Person.class).one(person)// - .then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) - .as(transactionalOperator::transactional).block(); - } - - // @Transactional - public List saveWithLogs(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return Flux - .merge(personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeConvert")), // - personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterConvert")), // - personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeInsert")), // - personOperationsRx.insertById(Person.class).one(person), // - personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterInsert"))) // - .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // - .as(transactionalOperator::transactional).collectList().block(); - - } - - public List saveWithErrorLogs(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return Flux - .merge(personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeConvert")), // - personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterConvert")), // - personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeInsert")), // - personOperationsRx.insertById(Person.class).one(person), // - personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterInsert"))) // - .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // - . flatMap(it -> Mono.error(new SimulateFailureException())).as(transactionalOperator::transactional) - .collectList().block(); - - } - - // org.springframework.beans.factory.NoUniqueBeanDefinitionException: - // No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single - // matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) - public Person declarativeSavePerson(Person person) { - assertInAnnotationTransaction(true); - return personOperations.insertById(Person.class).one(person); - } - - public Person savePersonBlocking(Person person) { - if (1 == 1) - throw new RuntimeException("not implemented"); - assertInAnnotationTransaction(true); - return personOperations.insertById(Person.class).one(person); - - } - - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) - public Person declarativeSavePersonErrors(Person person) { - assertInAnnotationTransaction(true); - Person p = personOperations.insertById(Person.class).one(person); // - SimulateFailureException.throwEx(); - return p; - } - - @Autowired CouchbaseCallbackTransactionManager callbackTm; - - /** - * to execute while ThreadReplaceloop() is running should force a retry - * - * @param person - * @return - */ - @Transactional(transactionManager = BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) - public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { - assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); - System.err.println("declarativeFindReplacePersonCallback cluster : " - + callbackTm.template().getCouchbaseClientFactory().getCluster().block()); - System.err.println("declarativeFindReplacePersonCallback resourceHolder : " - + org.springframework.transaction.support.TransactionSynchronizationManager - .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); - Person p = personOperations.findById(Person.class).one(person.getId().toString()); - return personOperations.replaceById(Person.class).one(p); - } - - /** - * to execute while ThreadReplaceloop() is running should force a retry - * - * @param person - * @return - */ - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) - public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { - assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePersonReactive try: " + tryCount.incrementAndGet()); - /* NoTransactionInContextException - TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { - System.err.println("declarativeFindReplacePersonReactive reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); - return Mono.just(sm); - }).block(); - */ - return personOperationsRx.findById(Person.class).one(person.getId().toString()) - .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p)); - } - - /** - * to execute while ThreadReplaceloop() is running should force a retry - * - * @param person - * @return - */ - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) // doesn't retry - public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { - assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet()); - Person p = personOperations.findById(Person.class).one(person.getId().toString()); - return personOperations.replaceById(Person.class).one(p); - } - - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // doesn't retry - public Mono declarativeSavePersonReactive(Person person) { - assertInAnnotationTransaction(true); - return personOperationsRx.insertById(Person.class).one(person); - } - - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) - public Mono declarativeSavePersonErrorsReactive(Person person) { - assertInAnnotationTransaction(true); - Mono p = personOperationsRx.insertById(Person.class).one(person); // - SimulateFailureException.throwEx(); - return p; - } - - void assertInAnnotationTransaction(boolean inTransaction) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - for (StackTraceElement ste : stack) { - if (ste.getClassName().startsWith("org.springframework.transaction.interceptor")) { - if (inTransaction) { - return; - } - } - } - if (!inTransaction) { - return; - } - throw new RuntimeException( - "in transaction = " + (!inTransaction) + " but expected in annotation transaction = " + inTransaction); - } - - } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java index 9da5bbb73..e5fa94953 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -23,17 +23,11 @@ import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; -import org.springframework.data.domain.Persistable; -import org.springframework.test.context.transaction.AfterTransaction; -import org.springframework.test.context.transaction.BeforeTransaction; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -95,7 +89,7 @@ public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaInte @Autowired Cluster myCluster; - /* DO NOT @Autowired */ PersonService personService; + /* DO NOT @Autowired */ PersonServiceInner personServiceInner; static GenericApplicationContext context; @Autowired ReactiveCouchbaseTemplate operations; @@ -104,7 +98,7 @@ public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaInte public static void beforeAll() { callSuperBeforeAll(new Object() {}); context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionReactiveIntegrationTests.Config.class, - CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); + PersonServiceInner.class); } @AfterAll @@ -114,7 +108,7 @@ public static void afterAll() { @BeforeEach public void beforeEachTest() { - personService = context.getBean(CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); // getting it via autowired results in no @Transactional + personServiceInner = context.getBean(PersonServiceInner.class); // getting it via autowired results in no @Transactional operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); @@ -124,7 +118,7 @@ public void beforeEachTest() { @Test // DATAMONGO-2265 public void shouldRollbackAfterException() { - personService.savePersonErrors(new Person(null, "Walter", "White")) // + personServiceInner.savePersonErrors(new Person(null, "Walter", "White")) // .as(StepVerifier::create) // .verifyError(RuntimeException.class); // operations.findByQuery(Person.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); @@ -140,7 +134,7 @@ public void shouldRollbackAfterException() { public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { Person p = new Person(null, "Walter", "White"); try { - personService.declarativeSavePersonErrors(p) // + personServiceInner.declarativeSavePersonErrors(p) // .as(StepVerifier::create) // .expectComplete(); // .verifyError(RuntimeException.class); @@ -157,7 +151,7 @@ public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { @Test // DATAMONGO-2265 public void commitShouldPersistTxEntries() { - personService.savePerson(new Person(null, "Walter", "White")) // + personServiceInner.savePerson(new Person(null, "Walter", "White")) // .as(StepVerifier::create) // .expectNextCount(1) // .verifyComplete(); @@ -173,7 +167,7 @@ public void commitShouldPersistTxEntries() { @Test // DATAMONGO-2265 public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { - personService.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // + personServiceInner.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // .expectNextCount(1) // .verifyComplete(); @@ -187,7 +181,7 @@ public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { @Test // DATAMONGO-2265 public void commitShouldPersistTxEntriesAcrossCollections() { - personService.saveWithLogs(new Person(null, "Walter", "White")) // + personServiceInner.saveWithLogs(new Person(null, "Walter", "White")) // .then() // .as(StepVerifier::create) // .verifyComplete(); @@ -206,7 +200,7 @@ public void commitShouldPersistTxEntriesAcrossCollections() { @Test // DATAMONGO-2265 public void rollbackShouldAbortAcrossCollections() { - personService.saveWithErrorLogs(new Person(null, "Walter", "White")) // + personServiceInner.saveWithErrorLogs(new Person(null, "Walter", "White")) // .then() // .as(StepVerifier::create) // .verifyError(); @@ -225,7 +219,7 @@ public void rollbackShouldAbortAcrossCollections() { @Test // DATAMONGO-2265 public void countShouldWorkInsideTransaction() { - personService.countDuringTx(new Person(null, "Walter", "White")) // + personServiceInner.countDuringTx(new Person(null, "Walter", "White")) // .as(StepVerifier::create) // .expectNext(1L) // .verifyComplete(); @@ -235,7 +229,7 @@ public void countShouldWorkInsideTransaction() { public void emitMultipleElementsDuringTransaction() { try { - personService.saveWithLogs(new Person(null, "Walter", "White")) // + personServiceInner.saveWithLogs(new Person(null, "Walter", "White")) // .as(StepVerifier::create) // .expectNextCount(4L) // .verifyComplete(); @@ -250,7 +244,7 @@ public void errorAfterTxShouldNotAffectPreviousStep() { Person p = new Person(1, "Walter", "White"); remove(couchbaseTemplate, "_default", p.getId().toString()); - personService.savePerson(p) // + personServiceInner.savePerson(p) // //.delayElement(Duration.ofMillis(100)) // .then(Mono.error(new RuntimeException("my big bad evil error"))).as(StepVerifier::create) // .expectError() @@ -267,15 +261,15 @@ public void errorAfterTxShouldNotAffectPreviousStep() { } // @RequiredArgsConstructor - static class PersonService { + static class PersonServiceInner { final ReactiveCouchbaseOperations personOperationsRx; final ReactiveCouchbaseTransactionManager managerRx; final CouchbaseOperations personOperations; final CouchbaseTransactionManager manager; - public PersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, - ReactiveCouchbaseTransactionManager mgrRx) { + public PersonServiceInner(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, + ReactiveCouchbaseTransactionManager mgrRx) { personOperations = ops; manager = mgr; System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); @@ -314,7 +308,7 @@ public Mono countDuringTx(Person person) { TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, new DefaultTransactionDefinition()); - return personOperationsRx.save(person) // + return personOperationsRx.insertById(Person.class).one(person) // .then(personOperationsRx.count(new Query(), Person.class)) // .as(transactionalOperator::transactional); } @@ -324,11 +318,11 @@ public Flux saveWithLogs(Person person) { TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, new DefaultTransactionDefinition()); - return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId().toString(), "beforeConvert")), // - personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // - personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // - personOperationsRx.save(person), // - personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // + return Flux.merge(personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId().toString(), "beforeConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.insertById(Person.class).one(person), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterInsert"))) // .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // .as(transactionalOperator::transactional); } @@ -353,7 +347,7 @@ public Flux declarativeSavePerson(Person person) { TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, new DefaultTransactionDefinition()); - return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); + return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.insertById(Person.class).one(person)); } @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) diff --git a/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java b/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java new file mode 100644 index 000000000..6ec5d38a8 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/PersonService.java @@ -0,0 +1,292 @@ +package org.springframework.data.couchbase.transactions; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; + +//@Service +class PersonService { + + final CouchbaseOperations personOperations; + final CouchbaseTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; + final ReactiveCouchbaseOperations personOperationsRx; + final ReactiveCouchbaseTransactionManager managerRx; + + public PersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, + ReactiveCouchbaseTransactionManager mgrRx) { + personOperations = ops; + manager = mgr; + System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory() + .getCluster()); + System.err.println("manager cluster : " + manager.getDatabaseFactory() + .getCluster()); + System.err.println("manager Manager : " + manager); + + personOperationsRx = opsRx; + managerRx = mgrRx; + System.out + .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory() + .getCluster() + .block()); + System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory() + .getCluster() + .block()); + System.out.println("managerRx Manager : " + managerRx); + return; + } + + public Person savePersonErrors(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class) + .one(person)// + .flatMap(it -> Mono.error(new SimulateFailureException()))// + .as(transactionalOperator::transactional) + .block(); + } + + public Person savePerson(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + return personOperationsRx.insertById(Person.class) + .one(person)// + .as(transactionalOperator::transactional) + .block(); + } + + public Long countDuringTx(Person person) { + assertInAnnotationTransaction(false); + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class) + .one(person)// + .then(personOperationsRx.findByQuery(Person.class) + .withConsistency(REQUEST_PLUS) + .count()) + .as(transactionalOperator::transactional) + .block(); + } + + // @Transactional + public List saveWithLogs(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux + .merge(personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "beforeConvert")), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "afterConvert")), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "beforeInsert")), + // + personOperationsRx.insertById(Person.class) + .one(person), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .all()) // + .as(transactionalOperator::transactional) + .collectList() + .block(); + + } + + public List saveWithErrorLogs(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux + .merge(personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "beforeConvert")), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "afterConvert")), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "beforeInsert")), + // + personOperationsRx.insertById(Person.class) + .one(person), + // + personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), + "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class) + .all()) // + .flatMap(it -> Mono.error(new SimulateFailureException())) + .as(transactionalOperator::transactional) + .collectList() + .block(); + + } + + // org.springframework.beans.factory.NoUniqueBeanDefinitionException: + // No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single + // matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + return personOperations.insertById(Person.class) + .one(person); + } + + public Person savePersonBlocking(Person person) { + if (1 == 1) + throw new RuntimeException("not implemented"); + assertInAnnotationTransaction(true); + return personOperations.insertById(Person.class) + .one(person); + + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Person declarativeSavePersonErrors(Person person) { + assertInAnnotationTransaction(true); + Person p = personOperations.insertById(Person.class) + .one(person); // + SimulateFailureException.throwEx(); + return p; + } + + @Autowired + CouchbaseCallbackTransactionManager callbackTm; + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); + System.err.println("declarativeFindReplacePersonCallback cluster : " + + callbackTm.template() + .getCouchbaseClientFactory() + .getCluster() + .block()); + System.err.println("declarativeFindReplacePersonCallback resourceHolder : " + + org.springframework.transaction.support.TransactionSynchronizationManager + .getResource(callbackTm.template() + .getCouchbaseClientFactory() + .getCluster() + .block())); + Person p = personOperations.findById(Person.class) + .one(person.getId() + .toString()); + return personOperations.replaceById(Person.class) + .one(p); + } + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonReactive try: " + tryCount.incrementAndGet()); + /* NoTransactionInContextException + TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { + System.err.println("declarativeFindReplacePersonReactive reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); + return Mono.just(sm); + }).block(); + */ + return personOperationsRx.findById(Person.class) + .one(person.getId() + .toString()) + .flatMap(p -> personOperationsRx.replaceById(Person.class) + .one(p)); + } + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) // doesn't retry + public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class) + .one(person.getId() + .toString()); + return personOperations.replaceById(Person.class) + .one(p); + } + + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // doesn't retry + public Mono declarativeSavePersonReactive(Person person) { + assertInAnnotationTransaction(true); + return personOperationsRx.insertById(Person.class) + .one(person); + } + + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + public Mono declarativeSavePersonErrorsReactive(Person person) { + assertInAnnotationTransaction(true); + Mono p = personOperationsRx.insertById(Person.class) + .one(person); // + SimulateFailureException.throwEx(); + return p; + } + + void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread() + .getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName() + .startsWith("org.springframework.transaction.interceptor") || + ste.getClassName() + .startsWith("org.springframework.data.couchbase.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException( + "in_annotation_transaction = " + (!inTransaction) + " but expected in_annotation_transaction = " + inTransaction + "\n class: " + getClass().getName()); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionInterception.save b/src/test/java/org/springframework/data/couchbase/transactions/TransactionInterception.save new file mode 100644 index 000000000..ca64011c3 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionInterception.save @@ -0,0 +1,51 @@ +package org.springframework.data.couchbase.transactions; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.interceptor.CouchbaseTransactionInterceptor; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class TransactionInterception { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource, + CouchbaseTransactionManager txManager) { + TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(); + interceptor.setTransactionAttributeSource(transactionAttributeSource); + if (txManager != null) { + interceptor.setTransactionManager(txManager); + } + return interceptor; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionAttributeSource transactionAttributeSource() { + return new AnnotationTransactionAttributeSource(); + } + + @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( + TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { + + BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); + advisor.setTransactionAttributeSource(transactionAttributeSource); + advisor.setAdvice(transactionInterceptor); + // if (this.enableTx != null) { + // advisor.setOrder(this.enableTx.getNumber("order")); + // } + return advisor; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index d468d0346..dae0530bd 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -114,9 +114,9 @@ public static void beforeAll() { } catch (IOException ioe) { throw new RuntimeException(ioe); } - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + //ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + //couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + //reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); } /** From bc4dc011c78725eaab5b1232c69308185108dd9e Mon Sep 17 00:00:00 2001 From: Michael Reiche Date: Tue, 3 May 2022 21:37:04 -0700 Subject: [PATCH 7/9] Get scope and collection from pseudoArgs and some cleanup. --- .../com/couchbase/client/java/Cluster.java | 2 +- .../client/java/ClusterInterface.java | 4 + .../AttemptContextReactiveAccessor.java | 243 ++++ .../AttemptContextReactiveAccessor.java | 93 -- ...a => CouchbaseTransactionManager.pre-core} | 4 +- .../demo/CouchbaseTransactionalTemplate.java | 56 +- .../demo/SpringTransactionGetResult.java | 7 +- .../couchbase/CouchbaseClientFactory.java | 12 +- .../ReactiveCouchbaseClientFactory.java | 19 +- .../SimpleCouchbaseClientFactory.java | 43 +- .../SimpleReactiveCouchbaseClientFactory.java | 151 +- .../AbstractCouchbaseConfiguration.java | 28 +- .../core/AbstractTemplateSupport.java | 26 +- .../couchbase/core/CouchbaseTemplate.java | 9 +- .../core/CouchbaseTemplateSupport.java | 10 +- .../core/ExecutableFindByIdOperation.java | 4 +- .../ExecutableFindByIdOperationSupport.java | 11 +- .../core/ExecutableFindByQueryOperation.java | 4 +- ...ExecutableFindByQueryOperationSupport.java | 8 +- .../core/ExecutableInsertByIdOperation.java | 4 +- .../ExecutableInsertByIdOperationSupport.java | 8 +- .../core/ExecutableRemoveByIdOperation.java | 20 +- .../ExecutableRemoveByIdOperationSupport.java | 19 +- .../ExecutableRemoveByQueryOperation.java | 5 +- ...ecutableRemoveByQueryOperationSupport.java | 8 +- .../core/ExecutableReplaceByIdOperation.java | 4 +- ...ExecutableReplaceByIdOperationSupport.java | 8 +- .../data/couchbase/core/GenericSupport.java | 95 +- .../core/NonReactiveSupportWrapper.java | 15 +- .../core/ReactiveCouchbaseTemplate.java | 93 +- .../ReactiveCouchbaseTemplateSupport.java | 10 +- .../core/ReactiveFindByIdOperation.java | 4 +- .../ReactiveFindByIdOperationSupport.java | 121 +- .../core/ReactiveFindByQueryOperation.java | 4 +- .../ReactiveFindByQueryOperationSupport.java | 155 +-- .../core/ReactiveInsertByIdOperation.java | 4 +- .../ReactiveInsertByIdOperationSupport.java | 59 +- .../core/ReactiveRemoveByIdOperation.java | 18 +- .../ReactiveRemoveByIdOperationSupport.java | 102 +- .../core/ReactiveRemoveByQueryOperation.java | 4 +- ...ReactiveRemoveByQueryOperationSupport.java | 9 +- .../core/ReactiveReplaceByIdOperation.java | 4 +- .../ReactiveReplaceByIdOperationSupport.java | 148 +- .../core/ReactiveTemplateSupport.java | 11 +- .../ReactiveUpsertByIdOperationSupport.java | 4 +- .../data/couchbase/core/TemplateSupport.java | 8 +- .../couchbase/core/support/PseudoArgs.java | 22 +- .../core/support/WithTransaction.java | 4 +- .../repository/DynamicProxyable.java | 4 +- .../support/CouchbaseRepositoryBase.java | 2 +- .../support/DynamicInvocationHandler.java | 8 +- .../SimpleReactiveCouchbaseRepository.java | 8 +- .../support/TransactionResultHolder.java | 7 +- ...ssionImpl.java => ClientSessionImplx.java} | 117 +- ...ClientSession.java => ClientSessionx.java} | 2 +- .../CouchbaseAttemptContextReactive.java | 6 +- .../transaction/CouchbaseResourceHolder.java | 157 --- .../transaction/CouchbaseResourceHolderx.java | 120 ++ ...hbaseSimpleCallbackTransactionManager.java | 40 +- .../transaction/CouchbaseStuffHandle.java | 145 -- .../CouchbaseTransactionManager.java | 129 +- .../CouchbaseTransactionalOperator.java | 231 ++++ .../ReactiveCouchbaseClientUtils.java | 56 +- .../ReactiveCouchbaseResourceHolder.java | 127 +- .../ReactiveCouchbaseTransactionManager.java | 118 +- .../SessionAwareMethodInterceptor.java | 43 +- .../transaction/TransactionsWrapper.java | 234 ++-- ...ntSession.java => AsyncClientSession.save} | 0 ...chbaseCacheCollectionIntegrationTests.java | 1 - .../cache/CouchbaseCacheIntegrationTests.java | 12 +- .../domain/ReactiveAirportRepository.java | 2 +- ...chbaseRepositoryQueryIntegrationTests.java | 3 + .../data/couchbase/transactions/Config.java | 7 +- ...basePersonTransactionIntegrationTests.java | 606 ++++---- ...onTransactionReactiveIntegrationTests.java | 1221 +++++++++-------- ...uchbaseReactiveTransactionNativeTests.java | 32 +- ...eTemplateTransaction2IntegrationTests.java | 201 --- ...seTemplateTransactionIntegrationTests.java | 61 +- .../CouchbaseTransactionNativeTests.java | 266 ++++ .../CouchbaseTransactionNativeTests.save | 368 ----- .../transactions/PersonServiceStandalone.java | 244 ---- .../couchbase/util/JavaIntegrationTests.java | 23 + .../couchbase/util/UnmanagedTestCluster.java | 2 +- .../data/couchbase/util/Util.java | 21 + 84 files changed, 3039 insertions(+), 3289 deletions(-) create mode 100644 src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java delete mode 100644 src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java rename src/main/java/com/example/demo/{CouchbaseTransactionManager.java => CouchbaseTransactionManager.pre-core} (97%) rename src/main/java/org/springframework/data/couchbase/transaction/{ClientSessionImpl.java => ClientSessionImplx.java} (72%) rename src/main/java/org/springframework/data/couchbase/transaction/{ClientSession.java => ClientSessionx.java} (95%) delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolderx.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java rename src/main/java/org/springframework/data/couchbase/transaction/internal/{AsyncClientSession.java => AsyncClientSession.save} (100%) delete mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java delete mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save delete mode 100644 src/test/java/org/springframework/data/couchbase/transactions/PersonServiceStandalone.java diff --git a/src/main/java/com/couchbase/client/java/Cluster.java b/src/main/java/com/couchbase/client/java/Cluster.java index 0d3b3601c..0b7a889b2 100644 --- a/src/main/java/com/couchbase/client/java/Cluster.java +++ b/src/main/java/com/couchbase/client/java/Cluster.java @@ -285,7 +285,7 @@ public static Cluster connect(final Set seedNodes, final ClusterOption notNull(options, "ClusterOptions"); final ClusterOptions.Built opts = options.build(); - return new Cluster(extractClusterEnvironment(null, opts), opts.authenticator(), seedNodes); + return new Cluster(extractClusterEnvironment("", opts), opts.authenticator(), seedNodes); } /** diff --git a/src/main/java/com/couchbase/client/java/ClusterInterface.java b/src/main/java/com/couchbase/client/java/ClusterInterface.java index c6cb97e06..341c6880d 100644 --- a/src/main/java/com/couchbase/client/java/ClusterInterface.java +++ b/src/main/java/com/couchbase/client/java/ClusterInterface.java @@ -40,6 +40,8 @@ import com.couchbase.client.java.search.SearchOptions; import com.couchbase.client.java.search.SearchQuery; import com.couchbase.client.java.search.result.SearchResult; +import com.couchbase.client.java.transactions.Transactions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import java.time.Duration; import java.util.Set; @@ -104,4 +106,6 @@ public interface ClusterInterface { void waitUntilReady(Duration timeout); void waitUntilReady(Duration timeout, WaitUntilReadyOptions options); + + Transactions transactions(); } diff --git a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java new file mode 100644 index 000000000..0909f32e9 --- /dev/null +++ b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java @@ -0,0 +1,243 @@ +/* +/* + * Copyright 2021 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 com.couchbase.client.java.transactions; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionContext; +import com.couchbase.client.core.transaction.CoreTransactionsReactive; +import com.couchbase.client.core.transaction.config.CoreMergedTransactionConfig; +import com.couchbase.client.core.transaction.config.CoreTransactionOptions; +import com.couchbase.client.core.transaction.log.CoreTransactionLogger; +import com.couchbase.client.core.transaction.support.AttemptState; +import com.couchbase.client.java.codec.JsonSerializer; +import reactor.core.publisher.Mono; + +/** + * To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext + * + * @author Michael Reiche + */ +public class AttemptContextReactiveAccessor { + + public static ReactiveTransactionAttemptContext getACR(TransactionAttemptContext attemptContext) { + // return attemptContext.ctx(); + // todo gp is this access needed. Could hold the raw CoreTransactionAttemptContext instead. + return null; + } + + public static ReactiveTransactions reactive(Transactions transactions) { + try { + Field field = Transactions.class.getDeclaredField("reactive"); + field.setAccessible(true); + return (ReactiveTransactions) field.get(transactions); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static ReactiveTransactionAttemptContext reactive(TransactionAttemptContext atr) { + JsonSerializer serializer; + try { + Field field = TransactionAttemptContext.class.getDeclaredField("serializer"); + field.setAccessible(true); + serializer = (JsonSerializer) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + try { + Field field = TransactionAttemptContext.class.getDeclaredField("internal"); + field.setAccessible(true); + return new ReactiveTransactionAttemptContext((CoreTransactionAttemptContext) field.get(atr), serializer); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static TransactionAttemptContext blocking(ReactiveTransactionAttemptContext atr) { + JsonSerializer serializer; + try { + Field field = ReactiveTransactionAttemptContext.class.getDeclaredField("serializer"); + field.setAccessible(true); + serializer = (JsonSerializer) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + try { + Field field = ReactiveTransactionAttemptContext.class.getDeclaredField("internal"); + field.setAccessible(true); + return new TransactionAttemptContext((CoreTransactionAttemptContext) field.get(atr), serializer); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static CoreTransactionLogger getLogger(ReactiveTransactionAttemptContext attemptContextReactive) { + // todo gp needed? + return null; + // return attemptContextReactive; + } + + // todo gp needed? + @Stability.Internal + public static CoreTransactionAttemptContext newCoreTranactionAttemptContext(ReactiveTransactions transactions) { + // PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); + // MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); + // + // TransactionContext overall = new TransactionContext( + // transactions.cleanup().clusterData().cluster().environment().requestTracer(), + // transactions.cleanup().clusterData().cluster().environment().eventBus(), + // UUID.randomUUID().toString(), now(), Duration.ZERO, merged); + + String txnId = UUID.randomUUID().toString(); + // overall.LOGGER.info(configDebug(transactions.config(), perConfig)); + + CoreTransactionsReactive coreTransactionsReactive; + try { + Field field = ReactiveTransactions.class.getDeclaredField("internal"); + field.setAccessible(true); + coreTransactionsReactive = (CoreTransactionsReactive) field.get(transactions); + } catch (Throwable err) { + throw new RuntimeException(err); + } + + CoreTransactionOptions perConfig = new CoreTransactionOptions(Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.of(Duration.ofMinutes(10)), + Optional.empty(), + Optional.empty()); + + CoreMergedTransactionConfig merged = new CoreMergedTransactionConfig(coreTransactionsReactive.config(), + Optional.ofNullable(perConfig)); + CoreTransactionContext overall = new CoreTransactionContext( + coreTransactionsReactive.core().context().environment().requestTracer(), + coreTransactionsReactive.core().context().environment().eventBus(), UUID.randomUUID().toString(), merged, + coreTransactionsReactive.core().transactionsCleanup()); + // overall.LOGGER.info(configDebug(config, perConfig, cleanup.clusterData().cluster().core())); + + CoreTransactionAttemptContext coreTransactionAttemptContext = coreTransactionsReactive.createAttemptContext(overall, + merged, txnId); + return coreTransactionAttemptContext; + // ReactiveTransactionAttemptContext reactiveTransactionAttemptContext = new ReactiveTransactionAttemptContext( + // coreTransactionAttemptContext, null); + // return reactiveTransactionAttemptContext; + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + + public static ReactiveTransactionAttemptContext from(CoreTransactionAttemptContext coreTransactionAttemptContext, + JsonSerializer serializer) { + TransactionAttemptContext tac = new TransactionAttemptContext(coreTransactionAttemptContext, serializer); + return reactive(tac); + } + + public static CoreTransactionAttemptContext getCore(ReactiveTransactionAttemptContext atr) { + CoreTransactionAttemptContext coreTransactionsReactive; + try { + Field field = ReactiveTransactionAttemptContext.class.getDeclaredField("internal"); + field.setAccessible(true); + coreTransactionsReactive = (CoreTransactionAttemptContext) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return coreTransactionsReactive; + } + + public static CoreTransactionAttemptContext getCore(TransactionAttemptContext atr) { + CoreTransactionAttemptContext coreTransactionsReactive; + try { + Field field = TransactionAttemptContext.class.getDeclaredField("internal"); + field.setAccessible(true); + coreTransactionsReactive = (CoreTransactionAttemptContext) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return coreTransactionsReactive; + } + + public static Mono implicitCommit(ReactiveTransactionAttemptContext atr, boolean b) { + CoreTransactionAttemptContext coreTransactionsReactive = getCore(atr); + try { + // getDeclaredMethod() does not find it (because of primitive arg?) + // CoreTransactionAttemptContext.class.getDeclaredMethod("implicitCommit", Boolean.class); + Method[] methods = CoreTransactionAttemptContext.class.getDeclaredMethods(); + Method method = null; + for(Method m:methods){ + if( m.getName().equals("implicitCommit")){ + method = m; + break; + } + } + if(method == null){ + throw new RuntimeException("did not find implicitCommit method"); + } + method.setAccessible(true); + return (Mono)method.invoke(coreTransactionsReactive, b); + } catch (Throwable err) { + throw new RuntimeException(err); + } + + } + + public static AttemptState getState(ReactiveTransactionAttemptContext atr) { + CoreTransactionAttemptContext coreTransactionsReactive = getCore(atr); + try { + Field field = CoreTransactionAttemptContext.class.getDeclaredField("state"); + field.setAccessible(true); + return (AttemptState) field.get(coreTransactionsReactive); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext(CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) { + return new ReactiveTransactionAttemptContext(core, jsonSerializer); + } + + // todo gp if needed let's expose in the SDK + // static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { + // StringBuilder sb = new StringBuilder(); + // sb.append("library version: "); + // sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); + // sb.append(" config: "); + // sb.append("atrs="); + // sb.append(config.numAtrs()); + // sb.append(", metadataCollection="); + // sb.append(config.metadataCollection()); + // sb.append(", expiry="); + // sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); + // sb.append("msecs durability="); + // sb.append(config.durabilityLevel()); + // sb.append(" per-txn config="); + // sb.append(" durability="); + // sb.append(perConfig.durabilityLevel()); + // sb.append(", supported="); + // sb.append(Supported.SUPPORTED); + // return sb.toString(); + // } + +} diff --git a/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java deleted file mode 100644 index 136eff601..000000000 --- a/src/main/java/com/couchbase/transactions/AttemptContextReactiveAccessor.java +++ /dev/null @@ -1,93 +0,0 @@ -/* -/* - * Copyright 2021 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 com.couchbase.transactions; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -import com.couchbase.client.core.transaction.log.CoreTransactionLogger; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import com.couchbase.client.java.transactions.TransactionAttemptContext; - -/** - * To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext - * - * @author Michael Reiche - */ -public class AttemptContextReactiveAccessor { - - public static ReactiveTransactionAttemptContext getACR(TransactionAttemptContext attemptContext) { - // return attemptContext.ctx(); - // todo gp is this access needed. Could hold the raw CoreTransactionAttemptContext instead. - return null; - } - - public static TransactionAttemptContext from(ReactiveTransactionAttemptContext attemptContextReactive) { - // todo gp needed? - return null; - // return new TransactionAttemptContext(attemptContextReactive); - } - - public static CoreTransactionLogger getLogger(ReactiveTransactionAttemptContext attemptContextReactive) { - // todo gp needed? - return null; - // return attemptContextReactive; - } - // todo gp needed? - // @Stability.Internal - // public static ReactiveTransactionAttemptContext newAttemptContextReactive(TransactionsReactive transactions){ - // return null; - // PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); - // MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); - // - // TransactionContext overall = new TransactionContext( - // transactions.cleanup().clusterData().cluster().environment().requestTracer(), - // transactions.cleanup().clusterData().cluster().environment().eventBus(), - // UUID.randomUUID().toString(), now(), Duration.ZERO, merged); - // - // String txnId = UUID.randomUUID().toString(); - // overall.LOGGER.info(configDebug(transactions.config(), perConfig)); - // return transactions.createAttemptContext(overall, merged, txnId); - // } - - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); - } - - // todo gp if needed let's expose in the SDK - // static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { - // StringBuilder sb = new StringBuilder(); - // sb.append("library version: "); - // sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); - // sb.append(" config: "); - // sb.append("atrs="); - // sb.append(config.numAtrs()); - // sb.append(", metadataCollection="); - // sb.append(config.metadataCollection()); - // sb.append(", expiry="); - // sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); - // sb.append("msecs durability="); - // sb.append(config.durabilityLevel()); - // sb.append(" per-txn config="); - // sb.append(" durability="); - // sb.append(perConfig.durabilityLevel()); - // sb.append(", supported="); - // sb.append(Supported.SUPPORTED); - // return sb.toString(); - // } - -} diff --git a/src/main/java/com/example/demo/CouchbaseTransactionManager.java b/src/main/java/com/example/demo/CouchbaseTransactionManager.pre-core similarity index 97% rename from src/main/java/com/example/demo/CouchbaseTransactionManager.java rename to src/main/java/com/example/demo/CouchbaseTransactionManager.pre-core index 967fd06fd..ab9d84087 100644 --- a/src/main/java/com/example/demo/CouchbaseTransactionManager.java +++ b/src/main/java/com/example/demo/CouchbaseTransactionManager.pre-core @@ -4,12 +4,10 @@ import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionAttemptContext; -import com.couchbase.client.java.transactions.TransactionResult; -import com.couchbase.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; -import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.couchbase.transaction.ClientSessionImpl; diff --git a/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java index 4fa7cecfc..53e529d78 100644 --- a/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java +++ b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java @@ -1,14 +1,18 @@ package com.example.demo; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.codec.Transcoder; import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionGetResult; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class CouchbaseTransactionalTemplate { private final CouchbaseTemplate template; @@ -19,11 +23,11 @@ public CouchbaseTransactionalTemplate(CouchbaseTemplate template) { public SpringTransactionGetResult findById(String id, Class domainType) { try { - TransactionAttemptContext ctx = getContext(); - TransactionGetResult getResult = ctx.get(template.getCouchbaseClientFactory().getDefaultCollection(), id); + CoreTransactionAttemptContext ctx = getContext(); + CoreTransactionGetResult getResult = ctx.get( makeCollectionIdentifier(template.getCouchbaseClientFactory().getDefaultCollection().async()) , id).block(); // todo gp getResult.cas() is no longer exposed - required? - T t = template.support().decodeEntity(id, getResult.contentAsObject().toString(), 0, domainType, + T t = template.support().decodeEntity(id, new String(getResult.contentAsBytes()), 0, domainType, null, null, null); return new SpringTransactionGetResult<>(t, getResult); } catch (Exception e) { @@ -33,46 +37,32 @@ public SpringTransactionGetResult findById(String id, Class domainType } - public void replaceById(TransactionGetResult getResult, T entity) { - TransactionAttemptContext ctx = getContext(); - - ctx.replace(getResult, template.support().encodeEntity(entity).getContent()); + public void replaceById(CoreTransactionGetResult getResult, T entity) { + CoreTransactionAttemptContext ctx = getContext(); + Transcoder transCoder = template.getCouchbaseClientFactory().getCluster().environment().transcoder(); + Transcoder.EncodedValue encoded = transCoder.encode(template.support().encodeEntity(entity).export()); + ctx.replace(getResult, encoded.encoded()); } - private TransactionAttemptContext getContext() { - CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager + private CoreTransactionAttemptContext getContext() { + ReactiveCouchbaseResourceHolder resource = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager .getResource(template.getCouchbaseClientFactory()); - TransactionAttemptContext atr; + CoreTransactionAttemptContext atr; if (resource != null) { - atr = resource.getAttemptContext(); + atr = resource.getCore(); } else { - CouchbaseResourceHolder holder = (CouchbaseResourceHolder) TransactionSynchronizationManager + ReactiveCouchbaseResourceHolder holder = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager .getResource(template.getCouchbaseClientFactory().getCluster()); - atr = holder.getSession().getTransactionAttemptContext(); + atr = holder.getCore(); } return atr; } - public static ReactiveTransactionAttemptContext getContextReactive(ReactiveCouchbaseTemplate template) { - CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager - .getResource(template.getCouchbaseClientFactory()); - ReactiveTransactionAttemptContext atr = null; - if (resource != null) { - atr = resource.getAttemptContextReactive(); - } else { - CouchbaseResourceHolder holder = (CouchbaseResourceHolder) TransactionSynchronizationManager - .getResource(template.getCouchbaseClientFactory().getCluster()); - if (holder != null && holder.getSession() != null) { - atr = holder.getSession().getReactiveTransactionAttemptContext(); - } - } - return atr; - } - public static ClientSession getSession(ReactiveCouchbaseTemplate template) { - CouchbaseTransactionManager.CouchbaseResourceHolder resource = (CouchbaseTransactionManager.CouchbaseResourceHolder) TransactionSynchronizationManager + public static ReactiveCouchbaseResourceHolder getSession(ReactiveCouchbaseTemplate template) { + ReactiveCouchbaseResourceHolder resource = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager .getResource(template.getCouchbaseClientFactory()); - return resource != null ? resource.getSession() : null; + return resource; } } diff --git a/src/main/java/com/example/demo/SpringTransactionGetResult.java b/src/main/java/com/example/demo/SpringTransactionGetResult.java index 6e02d4d98..40056de5c 100644 --- a/src/main/java/com/example/demo/SpringTransactionGetResult.java +++ b/src/main/java/com/example/demo/SpringTransactionGetResult.java @@ -1,13 +1,14 @@ package com.example.demo; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; import com.couchbase.client.java.transactions.TransactionGetResult; public class SpringTransactionGetResult { private final T value; - private final TransactionGetResult inner; + private final CoreTransactionGetResult inner; - public SpringTransactionGetResult(T value, TransactionGetResult inner) { + public SpringTransactionGetResult(T value, CoreTransactionGetResult inner) { this.value = value; this.inner = inner; } @@ -16,7 +17,7 @@ public T getValue() { return value; } - public TransactionGetResult getInner() { + public CoreTransactionGetResult getInner() { return inner; } diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index 4136c24a1..17797b5e9 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -18,16 +18,15 @@ import java.io.Closeable; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; -import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.data.couchbase.transaction.ClientSessionOptions; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; /** * The {@link CouchbaseClientFactory} is the main way to get access to the managed SDK instance and resources. @@ -77,9 +76,10 @@ public interface CouchbaseClientFactory extends Closeable { */ PersistenceExceptionTranslator getExceptionTranslator(); - ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr); + CoreTransactionAttemptContext getCore(TransactionOptions options, + CoreTransactionAttemptContext atr); - //CouchbaseClientFactory with(CouchbaseStuffHandle txOp); + //CouchbaseClientFactory with(CouchbaseTransactionalOperator txOp); - //CouchbaseStuffHandle getTransactionalOperator(); + //CouchbaseTransactionalOperator getTransactionalOperator(); } diff --git a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java index f103099ad..6d4372223 100644 --- a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java @@ -15,15 +15,17 @@ */ package org.springframework.data.couchbase; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.ClusterInterface; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import org.springframework.data.couchbase.transaction.ClientSession; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.data.couchbase.transaction.ClientSessionOptions; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; import org.springframework.dao.support.PersistenceExceptionTranslator; @@ -81,7 +83,7 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* */ PersistenceExceptionTranslator getExceptionTranslator(); - Mono getSession(ClientSessionOptions options); + Mono getTransactionResources(TransactionOptions options); String getBucketName(); @@ -89,13 +91,19 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* void close() throws IOException; - ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext ctx); + ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, CoreTransactionAttemptContext ctx); /* * (non-Javadoc) * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.ClientSession) */ - ReactiveCouchbaseClientFactory withSession(ClientSession session); + ReactiveCouchbaseClientFactory withCore(ReactiveCouchbaseResourceHolder core); + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#with(com.mongodb.session.ClientSession) + */ + ReactiveCouchbaseClientFactory with(CouchbaseTransactionalOperator txOp); /* * (non-Javadoc) @@ -103,4 +111,5 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* */ boolean isTransactionActive(); + CouchbaseTransactionalOperator getTransactionalOperator(); } diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index 6c8243cc7..d9b067751 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -19,15 +19,10 @@ import java.time.temporal.ChronoUnit; import java.util.function.Supplier; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import com.couchbase.client.java.transactions.TransactionAttemptContext; -import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; -import com.couchbase.client.java.transactions.config.TransactionsConfig; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; -import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.data.couchbase.transaction.ClientSessionImpl; -import org.springframework.data.couchbase.transaction.ClientSessionOptions; import com.couchbase.client.core.env.Authenticator; import com.couchbase.client.core.env.OwnedSupplier; @@ -37,8 +32,11 @@ import com.couchbase.client.java.ClusterOptions; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; +import com.couchbase.client.java.codec.JsonSerializer; import com.couchbase.client.java.env.ClusterEnvironment; -import com.couchbase.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; /** * The default implementation of a {@link CouchbaseClientFactory}. @@ -52,6 +50,7 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { private final Bucket bucket; private final Scope scope; private final PersistenceExceptionTranslator exceptionTranslator; + //private JsonSerializer serializer = null; public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, final String bucketName) { @@ -62,7 +61,9 @@ public SimpleCouchbaseClientFactory(final String connectionString, final Authent final String bucketName, final String scopeName) { this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator) // todo gp disabling cleanupLostAttempts to simplify output during development - .environment(env -> env.transactionsConfig(TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)))))), bucketName, scopeName); + .environment(env -> env.transactionsConfig( + TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)))))), + bucketName, scopeName); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, @@ -71,10 +72,12 @@ public SimpleCouchbaseClientFactory(final String connectionString, final Authent new OwnedSupplier<>( Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator).environment(environment))), bucketName, scopeName); + //this.serializer = environment.jsonSerializer(); } public SimpleCouchbaseClientFactory(final Cluster cluster, final String bucketName, final String scopeName) { this(() -> cluster, bucketName, scopeName); + //this.serializer = cluster.environment().jsonSerializer(); } private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, @@ -128,15 +131,15 @@ public PersistenceExceptionTranslator getExceptionTranslator() { } @Override - public ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr) { - // todo gp needed? - return null; - // can't we just use ReactiveTransactionAttemptContext everywhere? Instead of creating TransactionAttemptContext(atr), then + public CoreTransactionAttemptContext getCore(TransactionOptions options, CoreTransactionAttemptContext atr) { + // can't we just use AttemptContextReactive everywhere? Instead of creating AttemptContext(atr), then // accessing at.getACR() ? -// TransactionAttemptContext at = AttemptContextReactiveAccessor -// .from(atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive())); -// -// return new ClientSessionImpl(this, at); + if (atr == null) { + atr = AttemptContextReactiveAccessor + .newCoreTranactionAttemptContext(AttemptContextReactiveAccessor.reactive(getCluster().transactions())); + } + + return atr; } // @Override @@ -152,9 +155,9 @@ public ClientSession getSession(ClientSessionOptions options, ReactiveTransactio @Override public void close() { // todo gp -// if (cluster instanceof OwnedSupplier) { -// cluster.get().disconnect(); -// } + // if (cluster instanceof OwnedSupplier) { + // cluster.get().disconnect(); + // } } private static Duration now() { diff --git a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java index 7c84084a0..e114a1d69 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java @@ -3,10 +3,14 @@ import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import com.couchbase.client.java.ClusterInterface; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import com.couchbase.transactions.AttemptContextReactiveAccessor; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import com.couchbase.client.java.codec.JsonSerializer; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.Transactions; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; import java.io.IOException; @@ -15,9 +19,6 @@ import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; -import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.data.couchbase.transaction.ClientSessionImpl; -import org.springframework.data.couchbase.transaction.ClientSessionOptions; import org.springframework.data.couchbase.transaction.SessionAwareMethodInterceptor; import org.springframework.util.ObjectUtils; @@ -31,12 +32,23 @@ public class SimpleReactiveCouchbaseClientFactory implements ReactiveCouchbaseCl final String bucketName; final String scopeName; final PersistenceExceptionTranslator exceptionTranslator; + JsonSerializer serializer; + Transactions transactions; + CouchbaseTransactionalOperator transactionalOperator; - public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName) { + public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName, + CouchbaseTransactionalOperator transactionalOperator) { this.cluster = Mono.just(cluster); this.bucketName = bucketName; this.scopeName = scopeName; this.exceptionTranslator = new CouchbaseExceptionTranslator(); + this.serializer = cluster.environment().jsonSerializer(); + this.transactions = cluster.transactions(); + this.transactionalOperator = transactionalOperator; + } + + public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName) { + this(cluster, bucketName, scopeName, null); } @Override @@ -105,22 +117,25 @@ public void close() { } @Override - public Mono getSession(ClientSessionOptions options) { // hopefully this gets filled in later - return Mono.from(Mono.just(new ClientSessionImpl(this, null))); // .startSession(options)); + public Mono getTransactionResources(TransactionOptions options) { // hopefully this + // gets filled in + // later + return Mono.just(new ReactiveCouchbaseResourceHolder(null)); } @Override - public ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr) { - // todo gp needed? - return null; -// ReactiveTransactionAttemptContext at = atr != null ? atr : AttemptContextReactiveAccessor.newAttemptContextReactive(transactions.reactive()); -// -// return new ClientSessionImpl(this, at); + public ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, + CoreTransactionAttemptContext atr) { + if (atr == null) { + atr = AttemptContextReactiveAccessor + .newCoreTranactionAttemptContext(AttemptContextReactiveAccessor.reactive(transactions)); + } + return new ReactiveCouchbaseResourceHolder(atr); } @Override - public ReactiveCouchbaseClientFactory withSession(ClientSession session) { - return new ClientSessionBoundCouchbaseClientFactory(session, this); + public ReactiveCouchbaseClientFactory withCore(ReactiveCouchbaseResourceHolder holder) { + return new CoreTransactionAttemptContextBoundCouchbaseClientFactory(holder, this, transactions); } @Override @@ -128,51 +143,56 @@ public boolean isTransactionActive() { return false; } - //@Override - //public CouchbaseStuffHandle getTransactionalOperator() { - // return transactionalOperator; - //} + @Override + public CouchbaseTransactionalOperator getTransactionalOperator() { + return transactionalOperator; + } - //@Override - //public ReactiveCouchbaseClientFactory with(CouchbaseStuffHandle txOp) { - // return new SimpleReactiveCouchbaseClientFactory((Cluster) getCluster().block(), bucketName, scopeName, txOp); - //} + @Override + public ReactiveCouchbaseClientFactory with(CouchbaseTransactionalOperator txOp) { + return new SimpleReactiveCouchbaseClientFactory((Cluster) getCluster().block(), bucketName, scopeName, txOp); + } - private T createProxyInstance(ClientSession session, T target, Class targetType) { + private T createProxyInstance(ReactiveCouchbaseResourceHolder session, T target, Class targetType) { ProxyFactory factory = new ProxyFactory(); factory.setTarget(target); factory.setInterfaces(targetType); factory.setOpaque(true); - factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, ClusterInterface.class, - this::proxyDatabase, Collection.class, this::proxyCollection)); + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ReactiveCouchbaseResourceHolder.class, + ClusterInterface.class, this::proxyDatabase, Collection.class, this::proxyCollection)); return targetType.cast(factory.getProxy(target.getClass().getClassLoader())); } - private Collection proxyCollection(ClientSession session, Collection c) { + private Collection proxyCollection(ReactiveCouchbaseResourceHolder session, Collection c) { return createProxyInstance(session, c, Collection.class); } - private ClusterInterface proxyDatabase(ClientSession session, ClusterInterface cluster) { + private ClusterInterface proxyDatabase(ReactiveCouchbaseResourceHolder session, ClusterInterface cluster) { return createProxyInstance(session, cluster, ClusterInterface.class); } /** - * {@link ClientSession} bound TODO decorating the database with a {@link SessionAwareMethodInterceptor}. + * {@link CoreTransactionAttemptContext} bound TODO decorating the database with a + * {@link SessionAwareMethodInterceptor}. * * @author Christoph Strobl * @since 2.1 */ - static final class ClientSessionBoundCouchbaseClientFactory implements ReactiveCouchbaseClientFactory { + static final class CoreTransactionAttemptContextBoundCouchbaseClientFactory + implements ReactiveCouchbaseClientFactory { - private final ClientSession session; + private final ReactiveCouchbaseResourceHolder transactionResources; private final ReactiveCouchbaseClientFactory delegate; + // private final Transactions transactions; - ClientSessionBoundCouchbaseClientFactory(ClientSession session, ReactiveCouchbaseClientFactory delegate) { - this.session = session; + CoreTransactionAttemptContextBoundCouchbaseClientFactory(ReactiveCouchbaseResourceHolder transactionResources, + ReactiveCouchbaseClientFactory delegate, Transactions transactions) { + this.transactionResources = transactionResources; this.delegate = delegate; + // this.transactions = transactions; } /* @@ -235,25 +255,28 @@ public void close() throws IOException { /* * (non-Javadoc) - * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getSession(com.mongodb.ClientSessionOptions) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getSession(com.mongodb.CoreTransactionAttemptContextOptions) */ + @Override - public Mono getSession(ClientSessionOptions options) { - return Mono.just(getSession(options, null)); + public Mono getTransactionResources(TransactionOptions options) { + return Mono.just(transactionResources); } @Override - public ClientSession getSession(ClientSessionOptions options, ReactiveTransactionAttemptContext atr) { - return delegate.getSession(options, atr); + public ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, + CoreTransactionAttemptContext atr) { + ReactiveCouchbaseResourceHolder holder = delegate.getTransactionResources(options, atr); + return holder; } /* * (non-Javadoc) - * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.ClientSession) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.CoreTransactionAttemptContext) */ @Override - public ReactiveCouchbaseClientFactory withSession(ClientSession session) { - return delegate.withSession(session); + public ReactiveCouchbaseClientFactory withCore(ReactiveCouchbaseResourceHolder core) { + return delegate.withCore(core); } /* @@ -262,46 +285,46 @@ public ReactiveCouchbaseClientFactory withSession(ClientSession session) { */ @Override public boolean isTransactionActive() { - return session != null && session.hasActiveTransaction(); + return transactionResources != null && transactionResources.hasActiveTransaction(); } - //@Override - //public CouchbaseStuffHandle getTransactionalOperator() { - // return delegate.getTransactionalOperator(); - //} + @Override + public CouchbaseTransactionalOperator getTransactionalOperator() { + return delegate.getTransactionalOperator(); + } - //@Override - //public ReactiveCouchbaseClientFactory with(CouchbaseStuffHandle txOp) { - // return delegate.with(txOp); - //} + @Override + public ReactiveCouchbaseClientFactory with(CouchbaseTransactionalOperator txOp) { + return delegate.with(txOp); + } private ClusterInterface decorateDatabase(ClusterInterface database) { - return createProxyInstance(session, database, ClusterInterface.class); + return createProxyInstance(transactionResources, database, ClusterInterface.class); } - private ClusterInterface proxyDatabase(ClientSession session, ClusterInterface database) { + private ClusterInterface proxyDatabase(ReactiveCouchbaseResourceHolder session, ClusterInterface database) { return createProxyInstance(session, database, ClusterInterface.class); } - private Collection proxyCollection(ClientSession session, Collection collection) { + private Collection proxyCollection(ReactiveCouchbaseResourceHolder session, Collection collection) { return createProxyInstance(session, collection, Collection.class); } - private T createProxyInstance(ClientSession session, T target, Class targetType) { + private T createProxyInstance(ReactiveCouchbaseResourceHolder session, T target, Class targetType) { ProxyFactory factory = new ProxyFactory(); factory.setTarget(target); factory.setInterfaces(targetType); factory.setOpaque(true); - factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ClientSession.class, + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ReactiveCouchbaseResourceHolder.class, ClusterInterface.class, this::proxyDatabase, Collection.class, this::proxyCollection)); return targetType.cast(factory.getProxy(target.getClass().getClassLoader())); } - public ClientSession getSession() { - return this.session; + public ReactiveCouchbaseResourceHolder getTransactionResources() { + return this.transactionResources; } public ReactiveCouchbaseClientFactory getDelegate() { @@ -315,9 +338,9 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; - ClientSessionBoundCouchbaseClientFactory that = (ClientSessionBoundCouchbaseClientFactory) o; + CoreTransactionAttemptContextBoundCouchbaseClientFactory that = (CoreTransactionAttemptContextBoundCouchbaseClientFactory) o; - if (!ObjectUtils.nullSafeEquals(this.session, that.session)) { + if (!ObjectUtils.nullSafeEquals(this.transactionResources, that.transactionResources)) { return false; } return ObjectUtils.nullSafeEquals(this.delegate, that.delegate); @@ -325,14 +348,14 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = ObjectUtils.nullSafeHashCode(this.session); + int result = ObjectUtils.nullSafeHashCode(this.transactionResources); result = 31 * result + ObjectUtils.nullSafeHashCode(this.delegate); return result; } public String toString() { - return "SimpleReactiveCouchbaseDatabaseFactory.ClientSessionBoundCouchDbFactory(session=" + this.getSession() - + ", delegate=" + this.getDelegate() + ")"; + return "SimpleReactiveCouchbaseDatabaseFactory.CoreTransactionAttemptContextBoundCouchDbFactory(session=" + + this.getTransactionResources() + ", delegate=" + this.getDelegate() + ")"; } } } diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 3362fd663..2f8ce5196 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -25,8 +25,12 @@ import java.util.HashSet; import java.util.Set; +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.transaction.config.CoreTransactionsConfig; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.transactions.config.TransactionOptions; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -164,6 +168,8 @@ public ClusterEnvironment couchbaseClusterEnvironment() { throw new CouchbaseException("non-shadowed Jackson not present"); } builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper())); + TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)); + builder.transactionsConfig(transactionsConfig()); configureEnvironment(builder); return builder.build(); } @@ -338,6 +344,8 @@ public ObjectMapper couchbaseObjectMapper() { return mapper; } + /***** ALL THIS TX SHOULD BE MOVED OUT INTO THE IMPL OF AbstractCouchbaseConfiguration *****/ + // todo gp how to DI this into the Cluster creation esp. as it creates a CoreTransactionConfig // @Bean // public TransactionsConfig transactionConfig() { @@ -357,20 +365,26 @@ ReactiveCouchbaseTransactionManager reactiveTransactionManager( // todo gp experimenting with making CouchbaseSimpleCallbackTransactionManager the default - but it doesn't play // nice with MR's changes to insert CouchbaseTransactionInterceptor - @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) - CouchbaseSimpleCallbackTransactionManager transactionManager(CouchbaseClientFactory clientFactory, TransactionOptions options) { + // todo mr THIS DOES NOT WORK WELL with @TestTransaction / @BeforeTransaction / @AfterTransaction etc. + // todo mr Maybe it is only useful with @Transactional? + @Bean(BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + CouchbaseSimpleCallbackTransactionManager callbackTransactionManager(ReactiveCouchbaseClientFactory clientFactory, TransactionOptions options) { return new CouchbaseSimpleCallbackTransactionManager(clientFactory, options); } + @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) + CouchbaseTransactionManager transactionManager(CouchbaseClientFactory clientFactory, TransactionOptions options) { + return new CouchbaseTransactionManager(clientFactory, options); + } + @Bean - public TransactionOptions transactionOptions(){ + public TransactionOptions transactionsOptions(){ return TransactionOptions.transactionOptions(); } -// @Bean(BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) -// CouchbaseSimpleCallbackTransactionManager simpleCallbackTransactionManager(CouchbaseClientFactory clientFactory) { -// return new CouchbaseSimpleCallbackTransactionManager(clientFactory); -// } + public TransactionsConfig.Builder transactionsConfig(){ + return TransactionsConfig.builder().durabilityLevel(DurabilityLevel.NONE).timeout(Duration.ofMinutes(20));// for testing + } /** * Blocking Transaction Manager diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 0ecfc2825..179729fec 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -28,7 +28,7 @@ import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -58,7 +58,7 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv abstract ReactiveCouchbaseTemplate getReactiveTemplate(); public T decodeEntityBase(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, ClientSession session) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { final CouchbaseDocument converted = new CouchbaseDocument(id); converted.setId(id); @@ -110,8 +110,8 @@ public T decodeEntityBase(String id, String source, long cas, Class entit } N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id, scope, collection); - if(session != null){ - session.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + if(holder != null){ + holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); } return accessor.getBean(); @@ -127,7 +127,7 @@ CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder, ClientSession session) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() @@ -147,8 +147,8 @@ public T applyResultBase(T entity, CouchbaseDocument converted, Object id, l if (transactionResultProperty != null) { accessor.setProperty(transactionResultProperty, System.identityHashCode(txResultHolder)); } - if(session != null){ - session.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + if(holder != null){ + holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); } maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted)); return (T) accessor.getBean(); @@ -159,7 +159,6 @@ public Long getCas(final Object entity) { final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - long cas = 0; if (versionProperty != null) { Object casObject = accessor.getProperty(versionProperty); @@ -170,6 +169,17 @@ public Long getCas(final Object entity) { return cas; } + public Object getId(final Object entity) { + final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + Object id = null; + if (idProperty != null) { + id = accessor.getProperty(idProperty); + } + return id; + } + public String getJavaNameForEntity(final Class clazz) { final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 9f8fc4ded..60c9d8c9c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -32,8 +32,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.core.support.PseudoArgs; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -57,6 +56,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex private final ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; private final QueryScanConsistency scanConsistency; private @Nullable CouchbasePersistentEntityIndexCreator indexCreator; + private CouchbaseTransactionalOperator couchbaseTransactionalOperator; public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter) { @@ -234,6 +234,11 @@ public TemplateSupport support() { return templateSupport; } + public CouchbaseTemplate with(CouchbaseTransactionalOperator couchbaseTransactionalOperator) { + this.couchbaseTransactionalOperator = couchbaseTransactionalOperator; + return this; + } + /** * Get the TransactionalOperator from
    * 1. The template.clientFactory
    diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 2bd616368..a7252320e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -27,7 +27,7 @@ import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.util.Assert; @@ -75,8 +75,8 @@ public T decodeEntity(String id, String source, long cas, Class entityCla @Override public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txHolder, ClientSession session) { - return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, session); + TransactionResultHolder txHolder, ReactiveCouchbaseResourceHolder holder) { + return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder); } @Override @@ -87,8 +87,8 @@ public T applyResult(T entity, CouchbaseDocument converted, Object id, long @Override public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder, ClientSession session) { - return applyResultBase(entity, converted, id, cas, txResultHolder, session); + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return applyResultBase(entity, converted, id, cas, txResultHolder, holder); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index affea0d8b..2cb6def40 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -27,7 +27,7 @@ import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Get Operations @@ -118,7 +118,7 @@ interface FindByIdWithTransaction extends TerminatingFindById, WithTransac * @throws IllegalArgumentException if field is {@literal null}. */ @Override - FindByIdWithProjection transaction(CouchbaseStuffHandle txCtx); + FindByIdWithProjection transaction(CouchbaseTransactionalOperator txCtx); } interface FindByIdTxOrNot extends FindByIdWithExpiry, FindByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index aedd2f3ec..87c9994ea 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -21,13 +21,10 @@ import java.util.List; import org.springframework.data.couchbase.core.ReactiveFindByIdOperationSupport.ReactiveFindByIdSupport; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; -import org.springframework.transaction.reactive.TransactionContextManager; -import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.java.kv.GetOptions; -import reactor.core.publisher.Mono; public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOperation { @@ -51,11 +48,11 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final GetOptions options; private final List fields; private final Duration expiry; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, - GetOptions options, List fields, Duration expiry, CouchbaseStuffHandle txCtx) { + GetOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -109,7 +106,7 @@ public FindByIdWithProjection withExpiry(final Duration expiry) { } @Override - public FindByIdWithExpiry transaction(CouchbaseStuffHandle txCtx) { + public FindByIdWithExpiry transaction(CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index edabda276..581731eb1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -30,7 +30,7 @@ import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; import org.springframework.data.couchbase.core.support.WithTransaction; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.lang.Nullable; import com.couchbase.client.java.query.QueryOptions; @@ -193,7 +193,7 @@ interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithT * @throws IllegalArgumentException if field is {@literal null}. */ @Override - TerminatingFindByQuery transaction(CouchbaseStuffHandle txCtx); + TerminatingFindByQuery transaction(CouchbaseTransactionalOperator txCtx); } interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index 5bc7c1ba9..3339663f4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -20,7 +20,7 @@ import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; @@ -61,12 +61,12 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final QueryOptions options; private final String[] distinctFields; private final String[] fields; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, - final CouchbaseStuffHandle txCtx) { + final CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.returnType = returnType; @@ -151,7 +151,7 @@ public FindByQueryWithProjecting distinct(final String[] distinctFields) { } @Override - public FindByQueryWithDistinct transaction(CouchbaseStuffHandle txCtx) { + public FindByQueryWithDistinct transaction(CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null!"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, fields, txCtx); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index 4427e946b..aa8f06caf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -30,7 +30,7 @@ import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Insert Operations @@ -103,7 +103,7 @@ interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpir interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { @Override - InsertByIdWithExpiry transaction(CouchbaseStuffHandle txCtx); + InsertByIdWithExpiry transaction(CouchbaseTransactionalOperator txCtx); } interface InsertByIdTxOrNot extends InsertByIdWithExpiry, InsertByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index 0a5181f4b..f8f72611c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -19,7 +19,7 @@ import java.util.Collection; import org.springframework.data.couchbase.core.ReactiveInsertByIdOperationSupport.ReactiveInsertByIdSupport; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -53,12 +53,12 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseStuffHandle txCtx) { + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -126,7 +126,7 @@ public InsertByIdWithDurability withExpiry(final Duration expiry) { } @Override - public InsertByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { + public InsertByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 9bd423912..ffb916a89 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -30,7 +30,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Remove Operations on KV service. @@ -64,6 +64,14 @@ interface TerminatingRemoveById extends OneAndAllId { @Override RemoveResult one(String id); + /** + * Remove one document based on the entity. Transactions need the entity for the cas. + * + * @param entity the document ID. + * @return result of the remove + */ + RemoveResult oneEntity(Object entity); + /** * Remove the documents in the collection. * @@ -73,6 +81,14 @@ interface TerminatingRemoveById extends OneAndAllId { @Override List all(Collection ids); + /** + * Remove documents based on the entities. Transactions need the entity for the cas. + * + * @param entities to remove. + * @return result of the remove + */ + List allEntities(Collection entities); + } /** @@ -105,7 +121,7 @@ interface RemoveByIdWithCas extends RemoveByIdWithDurability, WithCas { @Override - TerminatingRemoveById transaction(CouchbaseStuffHandle txCtx); + TerminatingRemoveById transaction(CouchbaseTransactionalOperator txCtx); } interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index b777948fe..2bb4ef4fa 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -19,7 +19,7 @@ import java.util.List; import org.springframework.data.couchbase.core.ReactiveRemoveByIdOperationSupport.ReactiveRemoveByIdSupport; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -58,12 +58,12 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas, CouchbaseStuffHandle txCtx) { + final DurabilityLevel durabilityLevel, Long cas, CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -83,11 +83,22 @@ public RemoveResult one(final String id) { return reactiveRemoveByIdSupport.one(id).block(); } + @Override + public RemoveResult oneEntity(final Object entity) { + return reactiveRemoveByIdSupport.oneEntity(entity).block(); + } + @Override public List all(final Collection ids) { return reactiveRemoveByIdSupport.all(ids).collectList().block(); } + @Override + public List allEntities(final Collection entities) { + return reactiveRemoveByIdSupport.allEntities(entities).collectList().block(); + } + + @Override public RemoveByIdTxOrNot inCollection(final String collection) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, @@ -129,7 +140,7 @@ public RemoveByIdWithDurability withCas(Long cas) { } @Override - public RemoveByIdWithCas transaction(CouchbaseStuffHandle txCtx) { + public RemoveByIdWithCas transaction(CouchbaseTransactionalOperator txCtx) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index f7cf8950e..0fe7ed12c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -21,14 +21,13 @@ import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; -import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * RemoveBy Query Operations @@ -92,7 +91,7 @@ interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith */ interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { @Override - TerminatingRemoveByQuery transaction(CouchbaseStuffHandle txCtx); + TerminatingRemoveByQuery transaction(CouchbaseTransactionalOperator txCtx); } interface RemoveByQueryWithTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index 55148ee7b..e087a9897 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -19,7 +19,7 @@ import org.springframework.data.couchbase.core.ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport; import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; @@ -50,11 +50,11 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final String scope; private final String collection; private final QueryOptions options; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, - CouchbaseStuffHandle txCtx) { + CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.query = query; @@ -111,7 +111,7 @@ public RemoveByQueryInCollection inScope(final String scope) { } @Override - public TerminatingRemoveByQuery transaction(final CouchbaseStuffHandle txCtx) { + public TerminatingRemoveByQuery transaction(final CouchbaseTransactionalOperator txCtx) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index 5ca87b975..cea9174f7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -30,7 +30,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Replace Operations @@ -101,7 +101,7 @@ interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExp interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { @Override - TerminatingReplaceById transaction(CouchbaseStuffHandle txCtx); + TerminatingReplaceById transaction(CouchbaseTransactionalOperator txCtx); } interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index c65905670..85b78f610 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -19,7 +19,7 @@ import java.util.Collection; import org.springframework.data.couchbase.core.ReactiveReplaceByIdOperationSupport.ReactiveReplaceByIdSupport; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -53,12 +53,12 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseStuffHandle txCtx) { + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -113,7 +113,7 @@ public ReplaceByIdWithDurability withExpiry(final Duration expiry) { } @Override - public ReplaceByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { + public ReplaceByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx); diff --git a/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java b/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java index 7ac923e5c..adf8f0153 100644 --- a/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java @@ -1,63 +1,64 @@ package org.springframework.data.couchbase.core; -import com.couchbase.client.core.annotation.Stability; -import com.couchbase.client.java.Collection; -import com.couchbase.client.java.transactions.TransactionAttemptContext; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.lang.Nullable; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import reactor.core.publisher.Mono; -import java.util.Optional; import java.util.function.Function; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.lang.Nullable; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.java.ReactiveCollection; + // todo gp better name @Stability.Internal class GenericSupportHelper { - public final CouchbaseDocument converted; - public final Long cas; - public final Collection collection; - public final @Nullable TransactionAttemptContext ctx; - - public GenericSupportHelper(CouchbaseDocument doc, Long cas, Collection collection, @Nullable TransactionAttemptContext ctx) { - this.converted = doc; - this.cas = cas; - this.collection = collection; - this.ctx = ctx; - } + public final CouchbaseDocument converted; + public final Long cas; + public final ReactiveCollection collection; + public final @Nullable CoreTransactionAttemptContext ctx; + + public GenericSupportHelper(CouchbaseDocument doc, Long cas, ReactiveCollection collection, + @Nullable CoreTransactionAttemptContext ctx) { + this.converted = doc; + this.cas = cas; + this.collection = collection; + this.ctx = ctx; + } } // todo gp better name @Stability.Internal public class GenericSupport { - public static Mono one(ReactiveCouchbaseTemplate template, - String scopeName, - String collectionName, - ReactiveTemplateSupport support, - T object, - Function> nonTransactional, - Function> transactional) { - // todo gp how safe is this? I think we can switch threads potentially - Optional ctxr = Optional.ofNullable((TransactionAttemptContext) - org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); - - return template.getCouchbaseClientFactory().withScope(scopeName).getCollection(collectionName) - .flatMap(collection -> - support.encodeEntity(object) - .flatMap(converted -> { - GenericSupportHelper gsh = new GenericSupportHelper(converted, support.getCas(object), collection, ctxr.orElse(null)); - if (!ctxr.isPresent()) { - return nonTransactional.apply(gsh); - } else { - return transactional.apply(gsh); - } - })) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); - } + public static Mono one(Mono tmpl, CouchbaseTransactionalOperator transactionalOperator, + String scopeName, String collectionName, ReactiveTemplateSupport support, T object, + Function> nonTransactional, Function> transactional) { + // todo gp how safe is this? I think we can switch threads potentially + // Optional ctxr = Optional.ofNullable((TransactionAttemptContext) + // org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); + + return tmpl.flatMap(template -> template.getCouchbaseClientFactory().withScope(scopeName) + .getCollection(collectionName).flatMap(collection -> support.encodeEntity(object) + .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + GenericSupportHelper gsh = new GenericSupportHelper(converted, support.getCas(object), + collection.reactive(), s.getCore() != null ? s.getCore() + : (transactionalOperator != null ? transactionalOperator.getAttemptContext() : null)); + if (gsh.ctx == null) { + System.err.println("non-tx"); + return nonTransactional.apply(gsh); + } else { + System.err.println("tx"); + return transactional.apply(gsh); + } + })).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + })))); + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index 538141952..aee240adb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.core; import org.springframework.data.couchbase.core.convert.translation.TranslationService; -import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; @@ -49,8 +49,8 @@ public Mono decodeEntity(String id, String source, long cas, Class ent @Override public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, ClientSession session) { - return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, session)); + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } @Override @@ -61,8 +61,8 @@ public Mono applyResult(T entity, CouchbaseDocument converted, Object id, @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder, ClientSession session) { - return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder, session)); + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder, holder)); } @@ -71,6 +71,11 @@ public Long getCas(Object entity) { return support.getCas(entity); } + @Override + public Object getId(Object entity) { + return support.getId(entity); + } + @Override public String getJavaNameForEntity(Class clazz) { return support.getJavaNameForEntity(clazz); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index f0c643a26..75a4f1f1b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -16,35 +16,38 @@ package org.springframework.data.couchbase.core; -import com.couchbase.client.java.ClusterInterface; -import org.springframework.context.ApplicationListener; -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; -import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.data.couchbase.transaction.ReactiveCouchbaseClientUtils; -import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.transaction.SessionSynchronization; -import org.springframework.data.mapping.context.MappingContextEvent; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; +import java.util.function.Consumer; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseClientUtils; +import org.springframework.data.couchbase.transaction.SessionSynchronization; +import org.springframework.data.mapping.context.MappingContextEvent; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.ClusterInterface; import com.couchbase.client.java.Collection; import com.couchbase.client.java.query.QueryScanConsistency; -import java.util.function.Consumer; - -import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; - /** * template class for Reactive Couchbase operations * @@ -62,15 +65,15 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A private ThreadLocal> threadLocalArgs = new ThreadLocal<>(); private QueryScanConsistency scanConsistency; - //public ReactiveCouchbaseTemplate with(CouchbaseStuffHandle txOp) { + public ReactiveCouchbaseTemplate with(CouchbaseTransactionalOperator txOp) { // TODO: why does txOp go on the clientFactory? can't we just put it on the template?? - // return new ReactiveCouchbaseTemplate(getCouchbaseClientFactory().with(txOp), getConverter(), - // support().getTranslationService(), getConsistency()); - //} + return new ReactiveCouchbaseTemplate(getCouchbaseClientFactory().with(txOp), getConverter(), + support().getTranslationService(), getConsistency()); + } - //public CouchbaseStuffHandle txOperator() { - // return clientFactory.getTransactionalOperator(); - //} + public CouchbaseTransactionalOperator txOperator() { + return clientFactory.getTransactionalOperator(); + } public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { @@ -79,7 +82,7 @@ public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFact public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, final CouchbaseConverter converter, final TranslationService translationService, - final QueryScanConsistency scanConsistency) { + final QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); @@ -101,13 +104,29 @@ public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFact // } public Mono save(T entity) { - if (hasNonZeroVersionProperty(entity, templateSupport.converter)) { - return replaceById((Class) entity.getClass()).one(entity); - //} else if (getTransactionalOperator() != null) { - // return insertById((Class) entity.getClass()).one(entity); - } else { - return upsertById((Class) entity.getClass()).one(entity); + Assert.notNull(entity, "Entity must not be null!"); + Mono result; + final CouchbasePersistentEntity mapperEntity = getConverter().getMappingContext() + .getPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty(); + final boolean versionPresent = versionProperty != null; + final Long version = versionProperty == null || versionProperty.getField() == null ? null + : (Long) ReflectionUtils.getField(versionProperty.getField(), entity); + final boolean existingDocument = version != null && version > 0; + + Class clazz = entity.getClass(); + + if (!versionPresent) { // the entity doesn't have a version property + // No version field - no cas + result = (Mono) upsertById(clazz).one(entity); + } else if (existingDocument) { // there is a version property, and it is non-zero + // Updating existing document with cas + result = (Mono) replaceById(clazz).one(entity); + } else { // there is a version property, but it's zero or not set. + // Creating new document + result = (Mono) insertById(clazz).one(entity); } + return result; } public Mono count(Query query, Class domainType) { @@ -271,8 +290,8 @@ private Flux withSession(ReactiveSessionCallback action, ClientSession * (non-Javadoc) * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(com.mongodb.session.ClientSession) */ - public ReactiveCouchbaseOperations withSession(ClientSession session) { - return new ReactiveSessionBoundCouchbaseTemplate(session, ReactiveCouchbaseTemplate.this); + public ReactiveCouchbaseOperations withCore(ReactiveCouchbaseResourceHolder core) { + return new ReactiveSessionBoundCouchbaseTemplate(core, ReactiveCouchbaseTemplate.this); } /* @@ -288,10 +307,10 @@ public ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions) { */ /** - * {@link CouchbaseTemplate} extension bound to a specific {@link ClientSession} that is applied when interacting with - * the server through the driver API.
    + * {@link CouchbaseTemplate} extension bound to a specific {@link CoreTransactionAttemptContext} that is applied when + * interacting with the server through the driver API.
    * The prepare steps for {} and {} proxy the target and invoke the desired target method matching the actual arguments - * plus a {@link ClientSession}. + * plus a {@link CoreTransactionAttemptContext}. * * @author Christoph Strobl * @since 2.1 @@ -299,18 +318,18 @@ public ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions) { static class ReactiveSessionBoundCouchbaseTemplate extends ReactiveCouchbaseTemplate { private final ReactiveCouchbaseTemplate delegate; - private final ClientSession session; + private final ReactiveCouchbaseResourceHolder holder; /** - * @param session must not be {@literal null}. + * @param holder must not be {@literal null}. * @param that must not be {@literal null}. */ - ReactiveSessionBoundCouchbaseTemplate(ClientSession session, ReactiveCouchbaseTemplate that) { + ReactiveSessionBoundCouchbaseTemplate(ReactiveCouchbaseResourceHolder holder, ReactiveCouchbaseTemplate that) { - super(that.clientFactory.withSession(session), that.getConverter()); + super(that.clientFactory.withCore(holder), that.getConverter()); this.delegate = that; - this.session = session; + this.holder = holder; } /* diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index f7bb2c6ba..34f7edcd8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -17,7 +17,7 @@ package org.springframework.data.couchbase.core; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; import org.springframework.beans.BeansException; @@ -76,8 +76,8 @@ public Mono decodeEntity(String id, String source, long cas, Class ent @Override public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, ClientSession session) { - return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, session)); + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } @@ -89,8 +89,8 @@ public Mono applyResult(T entity, CouchbaseDocument converted, Object id, @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder, ClientSession session) { - return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, session)); + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, holder)); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index f96a70aca..4e54be960 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -83,7 +83,7 @@ interface FindByIdWithTransaction extends TerminatingFindById, WithTransac * @param txCtx * @return */ - TerminatingFindById transaction(CouchbaseStuffHandle txCtx); + TerminatingFindById transaction(CouchbaseTransactionalOperator txCtx); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index a5deb5c69..785ca8275 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -16,9 +16,9 @@ package org.springframework.data.couchbase.core; import static com.couchbase.client.java.kv.GetAndTouchOptions.getAndTouchOptions; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; import com.couchbase.client.core.transaction.CoreTransactionGetResult; -import com.couchbase.client.java.transactions.TransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionGetResult; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import reactor.core.publisher.Flux; @@ -29,13 +29,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.support.PseudoArgs; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.error.DocumentNotFoundException; @@ -67,12 +66,12 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { private final String collection; private final CommonOptions options; private final List fields; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveTemplateSupport support; private final Duration expiry; ReactiveFindByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, - CommonOptions options, List fields, Duration expiry, CouchbaseStuffHandle txCtx, + CommonOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; @@ -92,74 +91,52 @@ public Mono one(final String id) { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, txCtx, domainType); LOG.trace("findById {}", pArgs); -// return GenericSupport.one(template, scope, collection, support, object, -// (GenericSupportHelper support) -> { -// if (pArgs.getOptions() instanceof GetAndTouchOptions) { -// return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()).flatMap( -// result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); -// } else { -// return rc.get(id, (GetOptions) pArgs.getOptions()).flatMap( -// result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); -// } -// }, -// (GenericSupportHelper support) -> { -// return s.getReactiveTransactionAttemptContext().get(rc, id) -// // todo gp no cas -// .flatMap(result -> support.decodeEntity(id, result.contentAsObject().toString(), 0, -// domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result), s)); -// } -// })).onErrorResume(throwable -> { -// if (throwable instanceof DocumentNotFoundException) { -// return Mono.empty(); -// } -// return Mono.error(throwable); -// }); - - Optional ctxr = Optional.ofNullable((TransactionAttemptContext) - org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); - - com.couchbase.client.java.Collection coll = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).block(); - ReactiveCollection rc = coll.reactive(); - -// Mono tmpl = template.doGetTemplate(); - //ReactiveTransactionAttemptContext ctx = CouchbaseTransactionalTemplate.getContextReactive(template); - //ClientSession session = CouchbaseTransactionalTemplate.getSession(template); - - Mono reactiveEntity = Mono.defer(() -> { - if (!ctxr.isPresent()) { - if (pArgs.getOptions() instanceof GetAndTouchOptions) { - return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()).flatMap( - result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); - } else { - return rc.get(id, (GetOptions) pArgs.getOptions()).flatMap( - result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(), null)); - } - } else { - return Mono.defer(() -> { - TransactionGetResult result = ctxr.get().get(coll, id); - // todo gp no cas // todo mr - it's required by replace().one when comparing to internal.cas(). it's gone - // todo gp if we need this of course needs to be exposed nicely - Long cas=null; - try { - Method method = TransactionGetResult.class.getDeclaredMethod("internal"); - method.setAccessible(true); - CoreTransactionGetResult internal = (CoreTransactionGetResult) method.invoke(result); - cas = internal.cas(); - } - catch (Throwable err) { - throw new RuntimeException(err); - } - - return support.decodeEntity(id, result.contentAsObject().toString(), cas, - domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result), null) - .doOnNext(out -> { - // todo gp is this safe? are we on the right thread? - // org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(out, result); - }); - }); + ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).block().reactive(); + + // this will get me a template with a session holding tx + Mono tmpl = template.doGetTemplate(); + + Mono reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null) + .flatMap(s -> { + System.err.println("Session: "+s); + //Mono reactiveEntity = Mono.defer(() -> { + if (s == null || s.getCore() == null) { + if (pArgs.getOptions() instanceof GetAndTouchOptions) { + return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); + } else { + return rc.get(id, (GetOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); + } + } else { + return s.getCore().get(makeCollectionIdentifier(rc.async()), id) + .flatMap( result -> { + + // todo gp no cas // todo mr - it's required by replace().one when comparing to internal.cas(). it's gone + // todo gp if we need this of course needs to be exposed nicely + Long cas = result.cas(); + /* + try { + Method method = TransactionGetResult.class.getDeclaredMethod("internal"); + method.setAccessible(true); + CoreTransactionGetResult internal = (CoreTransactionGetResult) method.invoke(result); + cas = internal.cas(); + } catch (Throwable err) { + throw new RuntimeException(err); } +*/ + return support.decodeEntity(id, new String(result.contentAsBytes()), cas, domainType, pArgs.getScope(), + pArgs.getCollection(), new TransactionResultHolder(result), null).doOnNext(out -> { + // todo gp is this safe? are we on the right thread? + // org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(out, + // result); + }); }); + } + })); return reactiveEntity.onErrorResume(throwable -> { if (throwable instanceof DocumentNotFoundException) { @@ -214,7 +191,7 @@ public FindByIdWithProjection withExpiry(final Duration expiry) { } @Override - public FindByIdWithProjection transaction(CouchbaseStuffHandle txCtx) { + public FindByIdWithProjection transaction(CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, support); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index a9db4041a..a46d94944 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -152,7 +152,7 @@ interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithT * @return new instance of {@link ReactiveFindByQuery}. * @throws IllegalArgumentException if field is {@literal null}. */ - TerminatingFindByQuery transaction(CouchbaseStuffHandle txCtx); + TerminatingFindByQuery transaction(CouchbaseTransactionalOperator txCtx); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 6bc8dc1b0..05cd86781 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,19 +15,18 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.java.transactions.TransactionQueryOptions; -import com.couchbase.client.java.transactions.TransactionQueryResult; -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.error.CouchbaseException; @@ -35,6 +34,8 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +import com.couchbase.client.java.transactions.TransactionQueryResult; /** * {@link ReactiveFindByQueryOperation} implementations for Couchbase. @@ -71,13 +72,13 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final String[] distinctFields; private final String[] fields; private final QueryOptions options; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, - final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, - final CouchbaseStuffHandle txCtx, final ReactiveTemplateSupport support) { + final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, + final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, + final CouchbaseTransactionalOperator txCtx, final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); this.template = template; @@ -166,7 +167,7 @@ public FindByQueryWithDistinct distinct(final String[] distinctFields) { } @Override - public FindByQueryWithTransaction transaction(CouchbaseStuffHandle txCtx) { + public FindByQueryWithTransaction transaction(CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null!"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, fields, txCtx, support); @@ -187,66 +188,52 @@ public Flux all() { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - Mono allResult = null; - ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); - /* - if (pArgs.getTxOp() == null) { - QueryOptions opts = buildOptions(pArgs.getOptions()); - allResult = pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) - : rs.query(statement, opts); - } else { - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - allResult = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rs, statement, opts); - } - */ - // Mono cluster = template.doGetDatabase(); //doesn't work because there is no interface for Cluster Mono tmpl = template.doGetTemplate(); - // Mono ctx = TransactionContextManager.currentContext(); - //if (pArgs.getTxOp() == null && txOp == null) { // too early to find TxOp - transactional() has not yet been called - allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) - .flatMap(s -> { - if ( s == null || s.getReactiveTransactionAttemptContext() == null ) { - QueryOptions opts = buildOptions(pArgs.getOptions()); - return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) - : rs.query(statement, opts); - } else { - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - Mono tqr = s.getReactiveTransactionAttemptContext() - .query(statement, opts); - // todo gp do something with tqr - return Mono.empty(); - } - })); - Mono finalAllResult = allResult; - return finalAllResult.onErrorMap(throwable -> { + + Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + if (s.getCore() == null) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getCluster().block().environment().jsonSerializer())).query(statement, opts); + } + })); + + return allResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { return throwable; } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> { - String id = ""; - long cas = 0; - if (!query.isDistinct() && distinctFields == null) { - if (row.getString(TemplateUtils.SELECT_ID) == null) { - return Flux.error(new CouchbaseException( - "query did not project " + TemplateUtils.SELECT_ID + ". Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID + " and " + TemplateUtils.SELECT_CAS + " : " + statement)); - } - id = row.getString(TemplateUtils.SELECT_ID); - if (row.getLong(TemplateUtils.SELECT_CAS) == null) { - return Flux.error(new CouchbaseException( - "query did not project " + TemplateUtils.SELECT_CAS + ". Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID + " and " + TemplateUtils.SELECT_CAS + " : " + statement)); - } - cas = row.getLong(TemplateUtils.SELECT_CAS); - row.removeKey(TemplateUtils.SELECT_ID); - row.removeKey(TemplateUtils.SELECT_CAS); - } - return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), null); - }); + }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() + : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())).flatMap(row -> { + String id = ""; + long cas = 0; + if (!query.isDistinct() && distinctFields == null) { + if (row.getString(TemplateUtils.SELECT_ID) == null) { + return Flux.error(new CouchbaseException("query did not project " + TemplateUtils.SELECT_ID + + ". Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_ID + " and " + + TemplateUtils.SELECT_CAS + " : " + statement)); + } + id = row.getString(TemplateUtils.SELECT_ID); + if (row.getLong(TemplateUtils.SELECT_CAS) == null) { + return Flux.error(new CouchbaseException("query did not project " + TemplateUtils.SELECT_CAS + + ". Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_ID + " and " + + TemplateUtils.SELECT_CAS + " : " + statement)); + } + cas = row.getLong(TemplateUtils.SELECT_CAS); + row.removeKey(TemplateUtils.SELECT_ID); + row.removeKey(TemplateUtils.SELECT_CAS); + } + return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), + null); + }); } public QueryOptions buildOptions(QueryOptions options) { @@ -264,44 +251,32 @@ public Mono count() { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); - Mono countResult = null; Mono tmpl = template.doGetTemplate(); - /* - if (txCtx == null) { - QueryOptions opts = buildOptions(pArgs.getOptions()); - countResult = pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) - : rs.query(statement, opts); - } else { - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - countResult = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rc, statement, opts); - } - */ - countResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null) - .flatMap(s -> { - if ( s == null || s.getReactiveTransactionAttemptContext() == null ) { - QueryOptions opts = buildOptions(pArgs.getOptions()); - return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) - : rs.query(statement, opts); - } else { - TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - Mono tqr = s.getReactiveTransactionAttemptContext() - .query(statement, opts); - // todo gp do something with tqr - return Mono.empty(); - } - })); - Mono finalCountResult = countResult; - return Mono.defer(() -> finalCountResult.onErrorMap(throwable -> { + Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + if (s.getCore() == null) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getCluster().block().environment().jsonSerializer())).query(statement, opts); + } + })); + + return allResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { return throwable; } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong(row.getNames().iterator().next())) - .next()); + }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() + : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())) + .map(row -> row.getLong(row.getNames().iterator().next())).elementAt(0); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 86c53c2d7..a2818d21a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -104,7 +104,7 @@ interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpir interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { @Override - InsertByIdWithDurability transaction(CouchbaseStuffHandle txCtx); + InsertByIdWithDurability transaction(CouchbaseTransactionalOperator txCtx); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 16c57b629..6bddbbb0f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -15,11 +15,13 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.codec.Transcoder; import com.couchbase.client.java.transactions.TransactionGetResult; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.Method; import java.time.Duration; import java.util.Collection; @@ -28,7 +30,8 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; @@ -37,6 +40,8 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -64,13 +69,13 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final TransactionalOperator txOp; private final ReactiveTemplateSupport support; ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, CouchbaseStuffHandle txCtx, + final DurabilityLevel durabilityLevel, Duration expiry, CouchbaseTransactionalOperator txCtx, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; @@ -108,22 +113,33 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); LOG.trace("insertById {}", pArgs); + System.err.println("txOp: " + pArgs.getTxOp()); + Mono tmpl = template.doGetTemplate(); + + return GenericSupport.one(tmpl, pArgs.getTxOp(), pArgs.getScope(), pArgs.getCollection(), support, object, + (GenericSupportHelper support) -> support.collection + .insert(support.converted.getId(), support.converted.export(), + buildOptions(pArgs.getOptions(), support.converted)) + .flatMap(result -> this.support.applyResult(object, support.converted, support.converted.getId(), + result.cas(), null)), + (GenericSupportHelper support) -> support.ctx + .insert(makeCollectionIdentifier(support.collection.async()), support.converted.getId(), + template.getCouchbaseClientFactory().getCluster().block().environment().transcoder() + .encode(support.converted.export()).encoded()) + .flatMap(result -> this.support.applyResult(object, support.converted, support.converted.getId(), + getCas(result), new TransactionResultHolder(result), null))); + } - return GenericSupport.one(template, scope, collection, support, object, - (GenericSupportHelper support) -> { - return support.collection.reactive().insert(support.converted.getId(), support.converted.export(), buildOptions(pArgs.getOptions(), support.converted)) - .flatMap(result -> - this.support.applyResult(object, support.converted, support.converted.getId(), result.cas(), null)); - }, - (GenericSupportHelper support) -> { - return template.doGetTemplate() - // todo gp this runnable probably not great - .flatMap(tp -> Mono.defer(() -> { - TransactionGetResult result = support.ctx.insert(support.collection, support.converted.getId(), support.converted.getContent()); - // todo gp don't have result.cas() anymore - needed? - return this.support.applyResult(object, support.converted, support.converted.getId(), 0L, new TransactionResultHolder(result), null); - })); - }); + private Long getCas(CoreTransactionGetResult getResult) { + CoreTransactionGetResult internal; + try { + // Method method = CoreTransactionGetResult.class.getDeclaredMethod("internal"); + // method.setAccessible(true); + // internal = (CoreTransactionGetResult) method.invoke(getResult); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return getResult.cas(); } @Override @@ -161,7 +177,8 @@ public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel durabilityLevel, expiry, txCtx, support); } - // todo gp need to figure out how to handle options re transactions. E.g. many non-transactional insert options, like this, aren't supported + // todo gp need to figure out how to handle options re transactions. E.g. many non-transactional insert options, + // like this, aren't supported @Override public InsertByIdInScope withDurability(final PersistTo persistTo, final ReplicateTo replicateTo) { Assert.notNull(persistTo, "PersistTo must not be null."); @@ -178,7 +195,7 @@ public InsertByIdWithDurability withExpiry(final Duration expiry) { } @Override - public InsertByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { + public InsertByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, support); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index 3f96913bb..c827b02c7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -65,6 +65,13 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { @Override Mono one(String id); + /** + * Remove one document. Requires whole entity for transaction to have the cas. + * + * @param entity the entity + * @return result of the remove + */ + Mono oneEntity(Object entity); /** * Remove the documents in the collection. * @@ -73,6 +80,13 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { */ @Override Flux all(Collection ids); + /** + * Remove the documents in the collection. Requires whole entity for transaction to have the cas. + * + * @param ids the document IDs. + * @return result of the removes. + */ + Flux allEntities(Collection ids); } @@ -103,7 +117,7 @@ interface RemoveByIdWithCas extends RemoveByIdWithDurability { } interface RemoveByIdWithTransaction extends RemoveByIdWithCas, WithTransaction { - RemoveByIdWithCas transaction(CouchbaseStuffHandle txCtx); + RemoveByIdWithCas transaction(CouchbaseTransactionalOperator txCtx); } interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 8bc0b70a0..eb03491f7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,12 +15,16 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.transactions.TransactionGetResult; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.Method; import java.util.Collection; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,11 +34,12 @@ import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.ReactiveCollection; -import com.couchbase.client.java.codec.Transcoder; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -67,11 +72,11 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas, CouchbaseStuffHandle txCtx) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas, CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -88,28 +93,64 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { public Mono one(final String id) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("removeById {}", pArgs); - ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).block().reactive(); - Mono removeResult; - if (pArgs.getTxOp() == null) { - removeResult = rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); - } else { - Transcoder transcoder = template.getCouchbaseClientFactory().getCluster().block().environment().transcoder(); - // todo gp we definitely don't want to be creating TransactionGetResult. It's essential that this is passed - // from a previous ctx.get(). So we know if this doc is in a transaction and can safely detect - // write-write conflicts. This will be a blocker. - // Looks like replace is solving this with a getTransactionHolder? -// TransactionGetResult doc = new TransactionGetResult(id, null, 0, rc, tl, null, Optional.empty(), transcoder, -// null); - removeResult = pArgs.getTxOp().getAttemptContextReactive().remove(null).map(r -> new RemoveResult(id, 0, null)); - } - return removeResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).block() + .reactive(); + Mono tmpl = template.doGetTemplate(); + final Mono removeResult; + + Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + if (s.getCore() == null) { + System.err.println("non-tx remove"); + return rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); } else { - return throwable; - } - }); + System.err.println("tx remove"); + // todo gp we definitely don't want to be creating TransactionGetResult. It's essential that this is passed + // from a previous ctx.get(). So we know if this doc is in a transaction and can safely detect + // write-write conflicts. This will be a blocker. + // Looks like replace is solving this with a getTransactionHolder? + if ( cas == null || cas == 0 ){ + throw new IllegalArgumentException("cas must be supplied for tx remove"); + } + Mono gr = s.getCore().get(makeCollectionIdentifier(rc.async()), id); + + // todo gp no CAS + return gr.flatMap(getResult -> { + /* + CoreTransactionGetResult internal; + try { + Method method = CoreTransactionGetResult.class.getDeclaredMethod("internal"); + method.setAccessible(true); + internal = (CoreTransactionGetResult) method.invoke(getResult); + } + catch (Throwable err) { + throw new RuntimeException(err); + } +*/ + if (getResult.cas() != cas) { + System.err.println("internal: "+getResult.cas()+" object.cas: "+cas); + // todo gp really want to set internal state and raise a TransactionOperationFailed + throw new RetryTransactionException(); + } + return s.getCore().remove(getResult) + .map(r -> new RemoveResult(id, 0, null)); + }); + + }}).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + })); + return allResult; + } + + @Override + public Mono oneEntity(Object entity) { + ReactiveRemoveByIdSupport op = new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, template.support().getCas(entity), txCtx); + return op.one(template.support().getId(entity).toString()); } @Override @@ -117,6 +158,11 @@ public Flux all(final Collection ids) { return Flux.fromIterable(ids).flatMap(this::one); } + @Override + public Flux allEntities(Collection entities) { + return Flux.fromIterable(entities).flatMap(this::oneEntity); + } + private RemoveOptions buildRemoveOptions(RemoveOptions options) { return OptionsBuilder.buildRemoveOptions(options, persistTo, replicateTo, durabilityLevel, cas); } @@ -162,7 +208,7 @@ public RemoveByIdWithDurability withCas(Long cas) { } @Override - public RemoveByIdWithCas transaction(CouchbaseStuffHandle txCtx) { + public RemoveByIdWithCas transaction(CouchbaseTransactionalOperator txCtx) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 4f73e5214..766b6b9dd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import org.springframework.data.couchbase.core.query.Query; @@ -95,7 +95,7 @@ interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, W * @param txCtx - transaction */ @Override - TerminatingRemoveByQuery transaction(CouchbaseStuffHandle txCtx); + TerminatingRemoveByQuery transaction(CouchbaseTransactionalOperator txCtx); } interface RemoveByQueryTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index 1e3295e9c..d5f5e947c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -18,7 +18,7 @@ import com.couchbase.client.java.transactions.TransactionQueryOptions; import com.couchbase.client.java.transactions.TransactionQueryResult; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,7 +26,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; @@ -62,11 +61,11 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final String scope; private final String collection; private final QueryOptions options; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, - CouchbaseStuffHandle txCtx) { + CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.query = query; @@ -159,7 +158,7 @@ public RemoveByQueryInCollection inScope(final String scope) { } @Override - public RemoveByQueryWithConsistency transaction(final CouchbaseStuffHandle txCtx) { + public RemoveByQueryWithConsistency transaction(final CouchbaseTransactionalOperator txCtx) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options, txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index 23843e861..c18b66c76 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -101,7 +101,7 @@ interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExp interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { @Override - ReplaceByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx); + ReplaceByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx); } interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index 4df42f0cd..fa4288836 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -17,7 +17,6 @@ import com.couchbase.client.core.error.transaction.RetryTransactionException; import com.couchbase.client.core.transaction.CoreTransactionGetResult; -import com.couchbase.client.java.transactions.TransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionGetResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,15 +24,13 @@ import java.lang.reflect.Method; import java.time.Duration; import java.util.Collection; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.error.CouchbaseException; @@ -42,6 +39,8 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -69,12 +68,12 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final CouchbaseStuffHandle txCtx; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveTemplateSupport support; ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseStuffHandle txCtx, + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; @@ -89,124 +88,69 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { this.support = support; } - /* - @Override - public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, null, domainType); - LOG.trace("upsertById {}", pArgs); - Mono tmpl = template.doGetTemplate(); - Mono reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { - if (s == null || s.getAttemptContextReactive() == null) { - return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) - .flatMap(collection -> collection.reactive() - .replace(converted.getId(), converted.export(), - buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap( - result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); - } else { - return Mono.error(new CouchbaseException("No upsert in a transaction. Use insert or replace")); - } - }))); - - return reactiveEntity.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); - } - */ @Override public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("replaceById {}", pArgs); + Mono tmpl = template.doGetTemplate(); - return GenericSupport.one(template, scope, collection, support, object, + return GenericSupport.one(tmpl, pArgs.getTxOp(), pArgs.getScope(), pArgs.getCollection(), support, object, (GenericSupportHelper support) -> { CouchbaseDocument converted = support.converted; - return support.collection.reactive() + return support.collection .replace(converted.getId(), converted.export(), buildReplaceOptions(pArgs.getOptions(), object, converted)) .flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), null)); - }, - (GenericSupportHelper support) -> { + }, (GenericSupportHelper support) -> { CouchbaseDocument converted = support.converted; - + if ( support.cas == null || support.cas == 0 ){ + throw new IllegalArgumentException("cas must be supplied in object for tx replace. object="+object); + } // todo gp replace is a nightmare... // Where to put and how to pass the TransactionGetResult // - Idea: TransactionSynchronizationManager.bindResource // - Idea: use @Version as an index into Map // - As below, one idea is not to store it at all. - // Person could have been fetched outside of @Transactional block. Need to flat out prevent. Right?? - // - Maybe not. Could have the replaceById do a ctx.get(), and check the CAS matches the Person (will mandate @Version on Person). - // - Could always do that in fact. Then no need to hold onto TransactionGetResult anywhere - but slower too (could optimise later). + // Person could have been fetched outside of @Transactional block. Need to flat out prevent. Right?? + // - Maybe not. Could have the replaceById do a ctx.get(), and check the CAS matches the Person (will + // mandate @Version on Person). + // - Could always do that in fact. Then no need to hold onto TransactionGetResult anywhere - but slower too + // (could optimise later). // - And if had get-less replaces, could pass in the CAS. - // - Note: if Person was fetched outside the transaction, the transaction will inevitably expire (continuous CAS mismatch). + // - Note: if Person was fetched outside the transaction, the transaction will inevitably expire (continuous + // CAS mismatch). // -- Will have to doc that the user generally wants to do the read inside the txn. - // -- Can we detect this scenario and reject at runtime? That would also probably need storing something in Person. + // -- Can we detect this scenario and reject at runtime? That would also probably need storing something in + // Person. -// TransactionGetResult gr = (TransactionGetResult) org.springframework.transaction.support.TransactionSynchronizationManager.getResource(object); - TransactionGetResult gr = support.ctx.get(support.collection, converted.getId()); + // TransactionGetResult gr = (TransactionGetResult) + // org.springframework.transaction.support.TransactionSynchronizationManager.getResource(object); + Mono gr = support.ctx.get(makeCollectionIdentifier(support.collection.async()), converted.getId()); - // todo gp if we need this of course needs to be exposed nicely - CoreTransactionGetResult internal; - try { - Method method = TransactionGetResult.class.getDeclaredMethod("internal"); - method.setAccessible(true); - internal = (CoreTransactionGetResult) method.invoke(gr); - } - catch (Throwable err) { - throw new RuntimeException(err); - } - - if (internal.cas() != support.cas) { - System.err.println("internal: "+internal.cas()+" object.cas"+ support.cas); - // todo gp really want to set internal state and raise a TransactionOperationFailed - throw new RetryTransactionException(); - } - - support.ctx.replace(gr, converted.getContent()); - // todo gp no CAS - return this.support.applyResult(object, converted, converted.getId(), 0L, null, null); + // todo gp no CAS + return gr.flatMap(getResult -> { +/* + CoreTransactionGetResult internal; + try { + Method method = TransactionGetResult.class.getDeclaredMethod("internal"); + method.setAccessible(true); + internal = (CoreTransactionGetResult) method.invoke(getResult); + } + catch (Throwable err) { + throw new RuntimeException(err); + } +*/ + if (getResult.cas() != support.cas) { + System.err.println("internal: "+getResult.cas()+" object.cas: "+ support.cas+" "+converted); + // todo gp really want to set internal state and raise a TransactionOperationFailed + throw new RetryTransactionException(); + } + return support.ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().block().environment().transcoder() + .encode(support.converted.export()).encoded()); + }).flatMap(result -> this.support.applyResult(object, converted, converted.getId(), 0L, null, null)); }); -// Mono tmpl = template.doGetTemplate(); -// Mono reactiveEntity; -// -// Optional ctxr = Optional.ofNullable((TransactionAttemptContext) -// org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); -// -// CouchbaseDocument converted = support.encodeEntity(object).block(); -// reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { -// if (s == null || s.getReactiveTransactionAttemptContext() == null) { -// System.err.println("ReactiveReplaceById: not"); -// Mono op = template.getCouchbaseClientFactory() -// .withScope(pArgs.getScope()).getCollection(pArgs.getCollection()); -// return op.flatMap(collection -> collection.reactive() -// .replace(converted.getId(), converted.export(), -// buildReplaceOptions(pArgs.getOptions(), object, converted)) -// .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); -// } else { -// System.err.println("ReactiveReplaceById: transaction"); -// return s.getReactiveTransactionAttemptContext() -// .replace(s.transactionResultHolder(getTransactionHolder(object)).transactionGetResult(), -// converted.getContent()) -// // todo gp no CAS -// .flatMap(result -> support.applyResult(object, converted, converted.getId(), 0L, -// new TransactionResultHolder(result), s)); -// } -// })); -// -// return reactiveEntity.onErrorMap(throwable -> { -// if (throwable instanceof RuntimeException) { -// return template.potentiallyConvertRuntimeException((RuntimeException) throwable); -// } else { -// return throwable; -// } -// }); } private Integer getTransactionHolder(T object) { @@ -275,7 +219,7 @@ public ReplaceByIdWithDurability withExpiry(final Duration expiry) { } @Override - public ReplaceByIdWithExpiry transaction(final CouchbaseStuffHandle txCtx) { + public ReplaceByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { Assert.notNull(txCtx, "txCtx must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, support); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index d4ea03330..7dcdcb570 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -15,15 +15,14 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.transaction.ClientSession; import reactor.core.publisher.Mono; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; /** - * * @author Michael Reiche */ public interface ReactiveTemplateSupport { @@ -34,21 +33,23 @@ Mono decodeEntity(String id, String source, long cas, Class entityClas TransactionResultHolder txResultHolder); Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, ClientSession session); + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, TransactionResultHolder txResultHolder); Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder, ClientSession session); + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); Long getCas(Object entity); + Object getId(Object entity); + String getJavaNameForEntity(Class clazz); Integer getTxResultHolder(T source); - // Integer setTxResultHolder(T source); + // Integer setTxResultHolder(T source); TranslationService getTranslationService(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index 6dadac473..cd7e9a82a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -84,8 +84,8 @@ public Mono one(T object) { LOG.trace("upsertById {}", pArgs); Mono tmpl = template.doGetTemplate(); Mono reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getSession(null).flatMap(s -> { - if (s == null || s.getReactiveTransactionAttemptContext() == null) { + .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + if (s == null ) { return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).flatMap(collection -> collection.reactive() .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index d001c7c99..88edb0eb4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -19,7 +19,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; /** * @@ -31,14 +31,16 @@ public interface TemplateSupport { T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); - T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, ClientSession session); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder); - T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder, ClientSession session); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); Long getCas(Object entity); + Object getId(Object entity); + String getJavaNameForEntity(Class clazz); void maybeEmitEvent(CouchbaseMappingEvent event); diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index 0204da186..9d038bf39 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -23,19 +23,19 @@ import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import com.couchbase.client.core.io.CollectionIdentifier; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; public class PseudoArgs { private final OPTS options; private final String scopeName; private final String collectionName; - //private final CouchbaseStuffHandle transactionalOperator; + private final CouchbaseTransactionalOperator transactionalOperator; - public PseudoArgs(String scopeName, String collectionName, OPTS options, CouchbaseStuffHandle transactionalOperator) { + public PseudoArgs(String scopeName, String collectionName, OPTS options, CouchbaseTransactionalOperator transactionalOperator) { this.options = options; this.scopeName = scopeName; this.collectionName = collectionName; - //this.transactionalOperator = transactionalOperator; + this.transactionalOperator = transactionalOperator; } /** @@ -51,12 +51,12 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options, Couchba * @param domainType - entity that may have annotations */ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, - CouchbaseStuffHandle transactionalOperator, Class domainType) { + CouchbaseTransactionalOperator transactionalOperator, Class domainType) { String scopeForQuery = null; String collectionForQuery = null; OPTS optionsForQuery = null; - CouchbaseStuffHandle txOpForQuery = null; + CouchbaseTransactionalOperator txOpForQuery = null; // 1) repository from DynamicProxy via template threadLocal - has precedence over annotation @@ -74,7 +74,7 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle scopeForQuery = fromFirst(null, scopeForQuery, scope, getScopeFrom(domainType)); collectionForQuery = fromFirst(null, collectionForQuery, collection, getCollectionFrom(domainType)); optionsForQuery = fromFirst(null, options, optionsForQuery); - txOpForQuery = fromFirst( null, transactionalOperator, txOpForQuery /*, template.txOperator() */); + txOpForQuery = fromFirst( null, transactionalOperator, txOpForQuery , template.txOperator() ); // if a collection was specified but no scope, use the scope from the clientFactory @@ -97,7 +97,7 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle throw new CouchbaseException(new IllegalArgumentException("if scope is not default or null, then collection must be specified")); } this.options = optionsForQuery; - //this.transactionalOperator = txOpForQuery; + this.transactionalOperator = txOpForQuery; } @@ -125,13 +125,13 @@ public String getCollection() { /** * @return the attempt context */ - public CouchbaseStuffHandle getTxOp() { - return null; // transactionalOperator; + public CouchbaseTransactionalOperator getTxOp() { + return transactionalOperator; } @Override public String toString() { - return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions(); + return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions()+" txOp: "+transactionalOperator; } } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java index a4c7825ad..5662a6804 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core.support; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Interface for operations that take distinct fields @@ -29,5 +29,5 @@ public interface WithTransaction { * * @param txCtx */ - Object transaction(CouchbaseStuffHandle txCtx); + Object transaction(CouchbaseTransactionalOperator txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java index b4cd5fb98..6b190091d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -22,7 +22,7 @@ import org.springframework.data.couchbase.repository.support.DynamicInvocationHandler; import com.couchbase.client.java.CommonOptions; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * The generic parameter needs to be REPO which is either a CouchbaseRepository parameterized on T,ID or a @@ -100,7 +100,7 @@ default REPO withTransaction(TransactionalOperator ctx) { } */ - default REPO withTransaction(CouchbaseStuffHandle ctx) { + default REPO withTransaction(CouchbaseTransactionalOperator ctx) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, null, ctx)); return proxyInstance; diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index 3ea7bd707..b54777322 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -65,7 +65,7 @@ Class getJavaType() { } String getId(S entity) { - return getEntityInformation().getId(entity); + return String.valueOf(getEntityInformation().getId(entity)); } /** diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index 6e64386bb..d37e45552 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -29,7 +29,7 @@ import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import com.couchbase.client.java.CommonOptions; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Invocation Handler for scope/collection/options proxy for repositories @@ -46,10 +46,10 @@ public class DynamicInvocationHandler implements InvocationHandler { CommonOptions options; String collection; String scope; - CouchbaseStuffHandle ctx; + CouchbaseTransactionalOperator ctx; public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope, - CouchbaseStuffHandle ctx) { + CouchbaseTransactionalOperator ctx) { this.target = target; if (target instanceof CouchbaseRepository) { reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); @@ -106,7 +106,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (method.getName().equals("withTransaction")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, options, collection, scope, (CouchbaseStuffHandle) args[0])); + new DynamicInvocationHandler<>(target, options, collection, scope, (CouchbaseTransactionalOperator) args[0])); } Class[] paramTypes = null; diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index e4fc36cdb..1cc074fae 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -16,7 +16,6 @@ package org.springframework.data.couchbase.repository.support; -import com.couchbase.client.java.CommonOptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,14 +26,11 @@ import org.reactivestreams.Publisher; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; import org.springframework.data.domain.Sort; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; @@ -183,7 +179,7 @@ public Mono deleteById(Publisher publisher) { @Override public Mono delete(T entity) { Assert.notNull(entity, "Entity must not be null!"); - return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(getId(entity)) + return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).oneEntity(entity) .then(); } @@ -196,7 +192,7 @@ public Mono deleteAllById(Iterable ids) { @Override public Mono deleteAll(Iterable entities) { return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) - .all(Streamable.of(entities).map(this::getId).toList()).then(); + .allEntities((java.util.Collection)(Streamable.of(entities).toList())).then(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java index 1377cd91e..653bba57f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.repository.support; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.transactions.TransactionGetResult; import reactor.util.annotation.Nullable; @@ -27,11 +28,11 @@ */ public class TransactionResultHolder { - private final @Nullable TransactionGetResult getResult; + private final @Nullable CoreTransactionGetResult getResult; // todo gp needed? private final @Nullable QueryResult singleQueryResult; - public TransactionResultHolder(TransactionGetResult getResult) { + public TransactionResultHolder(CoreTransactionGetResult getResult) { // we don't need the content and we don't have access to the transcoder an txnMeta (and we don't need them either). // todo gp will need to expose a copy ctor if a copy is needed this.getResult = getResult; @@ -45,7 +46,7 @@ public TransactionResultHolder(QueryResult singleQueryResult) { this.singleQueryResult = singleQueryResult; } - public @Nullable TransactionGetResult transactionGetResult() { + public @Nullable CoreTransactionGetResult transactionGetResult() { return getResult; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImplx.java similarity index 72% rename from src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java rename to src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImplx.java index 75d97ed06..3aa6b29d8 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImpl.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImplx.java @@ -1,10 +1,13 @@ package org.springframework.data.couchbase.transaction; import com.couchbase.client.core.transaction.support.AttemptState; +import com.couchbase.client.core.transaction.support.TransactionAttemptContextFactory; import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionAttemptContext; import com.couchbase.client.java.transactions.Transactions; import com.couchbase.client.java.transactions.config.TransactionOptions; +import com.couchbase.client.java.transactions.config.TransactionsConfig; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; @@ -17,8 +20,6 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.Optional; -import java.util.UUID; import org.reactivestreams.Publisher; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -31,7 +32,7 @@ import com.couchbase.client.java.Scope; import com.couchbase.client.java.env.ClusterEnvironment; -public class ClientSessionImpl implements ClientSession { +public class ClientSessionImplx implements ClientSessionx { protected transient Log logger = LogFactory.getLog(AbstractReactiveTransactionManager.class); @@ -40,44 +41,28 @@ public class ClientSessionImpl implements ClientSession { boolean commitInProgress = false; boolean messageSentInCurrentTransaction = true; // needs to be true for commit // todo gp probably should not be duplicating CoreTransactionAttemptContext state outside of it - AttemptState transactionState = AttemptState.NOT_STARTED; + //AttemptState transactionState = AttemptState.NOT_STARTED; TransactionOptions transactionOptions; TransactionContext ctx; ReactiveTransactionAttemptContext atr = null; - TransactionAttemptContext at = null; Map getResultMap = new HashMap<>(); - public ClientSessionImpl(){} - - public ClientSessionImpl(ReactiveCouchbaseClientFactory couchbaseClientFactory, ReactiveTransactionAttemptContext atr) { - scopeRx = couchbaseClientFactory.getScope(); - // todo gp hopefully none of this is needed -// this.config = config == null -// ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() -// : config; -// MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); -// ClusterEnvironment environment = couchbaseClientFactory.getCluster().block().environment(); -// ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), -// now(), Duration.ZERO, merged); - // does this not need an non-reactive TransactionAttemptContext? + public ClientSessionImplx(){} + + public ClientSessionImplx(ReactiveCouchbaseClientFactory couchbaseClientFactory, ReactiveTransactionAttemptContext atr) { + this.scopeRx = couchbaseClientFactory.getScope(); this.atr = atr; + System.err.println("new "+this); } - public ClientSessionImpl(CouchbaseClientFactory couchbaseClientFactory, TransactionAttemptContext at) { - // todo gp hopefully none of this is needed -// this.transactions = transactions; - scope = couchbaseClientFactory.getScope(); -// this.config = config == null -// ? TransactionConfigBuilder.create().expirationTime(Duration.ofSeconds(120)).build() -// : config; -// MergedTransactionConfig merged = new MergedTransactionConfig(this.config, Optional.empty()); - ClusterEnvironment environment = couchbaseClientFactory.getCluster().environment(); -// ctx = new TransactionContext(environment.requestTracer(), environment.eventBus(), UUID.randomUUID().toString(), -// now(), Duration.ZERO, merged); - this.at = at; -// if(at != null){ -// this.atr = AttemptContextReactiveAccessor.getACR(at); -// } + public ClientSessionImplx(CouchbaseClientFactory couchbaseClientFactory, ReactiveTransactionAttemptContext atr) { + this.scope = couchbaseClientFactory.getScope(); + this.atr = atr; + System.err.println("NEW "+this); + } + + private Transactions getTransactions(Transactions transactions) { + return transactions; } @Override @@ -100,11 +85,6 @@ public void notifyOperationInitiated(Object var1) { } - //@Override - //public void setAttemptContextReactive(ReactiveTransactionAttemptContext atr){ - // this.atr = atr; - //} - @Override public ReactiveTransactionAttemptContext getReactiveTransactionAttemptContext(){ return atr; @@ -112,17 +92,9 @@ public ReactiveTransactionAttemptContext getReactiveTransactionAttemptContext(){ @Override public TransactionAttemptContext getTransactionAttemptContext(){ - return at; + return atr == null? null : AttemptContextReactiveAccessor.blocking(atr); } - - // setter that returns `this` - //@Override - //public ClientSession with(ReactiveTransactionAttemptContext atr){ - // setAttemptContextReactive(atr); - // return this; - //} - @Override public TransactionOptions getTransactionOptions() { return transactionOptions; @@ -136,15 +108,17 @@ public AsyncCluster getWrapped() { // todo gp @Override public void startTransaction() { - transactionState = AttemptState.PENDING; + System.err.println("startTransaction: "+this); + //transactionState = AttemptState.PENDING; } // todo gp @Override public Publisher commitTransaction() { - if (this.transactionState == AttemptState.ABORTED) { + AttemptState state = getState(); + if (state == AttemptState.ABORTED) { throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction"); - } else if (this.transactionState == AttemptState.NOT_STARTED) { + } else if (state == AttemptState.NOT_STARTED) { throw new IllegalStateException("There is no transaction started"); } else if (!this.messageSentInCurrentTransaction) { // seems there should have been a messageSent. We just do nothing(?) this.cleanupTransaction(AttemptState.COMMITTED); @@ -154,7 +128,7 @@ public Publisher commitTransaction() { if (0 == 1/* readConcern == null*/) { throw new CouchbaseException("Invariant violated. Transaction options read concern can not be null"); } else { - boolean alreadyCommitted = this.commitInProgress || this.transactionState == AttemptState.COMMITTED; + boolean alreadyCommitted = this.commitInProgress || state == AttemptState.COMMITTED; this.commitInProgress = true; // this will fail with ctx.serialized() being Optional.empty() // how does the commit happen in transactions.reactive().run() ? @@ -179,20 +153,22 @@ public Publisher commitTransaction() { } } - public Mono executeImplicitCommit(ReactiveTransactionAttemptContext ctx) { - return Mono.defer(() -> { + public Mono executeImplicitCommit(ReactiveTransactionAttemptContext ctx) { + if (logger.isDebugEnabled()) { logger.debug(String.format("About to commit ctx %s", ctx)); } // If app has not explicitly performed a commit, assume they want to do so anyway if (0 != 1 /*!ctx.isDone()*/) { if (0 == 1 /*ctx.serialized().isPresent()*/) { - return Mono.just(ctx); + return Mono.empty(); // Mono.just(ctx); } else { //System.err.println(ctx.attemptId()+ " doing implicit commit"); // ctx.LOGGER.trace(); - System.err.println("doing implicit commit"); + System.err.println("doing implicit commit: "+this); + return AttemptContextReactiveAccessor.implicitCommit(atr, false); + // todo gp ctx.commit() has gone in the SDK integration. Do we need this logic though? - return Mono.empty(); + //return Mono.empty(); // if(ctx != null) { // return ctx.commit() // .then(Mono.just(ctx)) @@ -206,9 +182,8 @@ public Mono executeImplicitCommit(ReactiveTra } else { System.err.println("Transaction already done"); //System.err.println(ctx.attemptId()+" Transaction already done"); // // ctx.LOGGER.trace(); - return Mono.just(ctx); + return Mono.empty(); // Mono.just(ctx); } - }); } @@ -217,7 +192,7 @@ public Mono executeImplicitCommit(ReactiveTra public Publisher abortTransaction() { System.err.println("**** abortTransaction ****"); // Assert.notNull(transactions, "transactions"); - Assert.notNull(ctx, "ctx"); +// Assert.notNull(ctx, "ctx"); // Assert.notNull(ctx.serialized(), "ctx.serialized()"); // if (ctx.serialized().isPresent()) { // Assert.notNull(ctx.serialized().get(), "ctx.serialized().get()"); @@ -229,13 +204,10 @@ public Publisher abortTransaction() { private Mono executeExplicitRollback(ReactiveTransactionAttemptContext atr) { // todo gp ctx.rollback() is removed + // todo mr - so what happens when the client requests that the tx be rolledback? + // todo mr - does throwing an exception result in rollback? + // todo mr - should an exception be thrown here on a request to rollback, when we can't do a rollback? return Mono.empty(); -// if(at != null){ -// at.rollback(); -// return Mono.empty(); -// } else { -// return atr.rollback(); -// } } @Override @@ -289,4 +261,19 @@ public TransactionResultHolder transactionResultHolder(TransactionResultHolder h private static Duration now() { return Duration.of(System.nanoTime(), ChronoUnit.NANOS); } + + public String toString(){ + StringBuffer sb = new StringBuffer(); + sb.append(this.getClass().getSimpleName()+"@"+System.identityHashCode(this)); + sb.append("{"); + sb.append("atr: "+ ( atr == null ? null : atr.toString().replace("com.couchbase.client.java.transactions.",""))); + sb.append(", state: "+(atr == null ? null : getState())); + sb.append("}"); + return sb.toString(); + } + + private AttemptState getState() { + AttemptState state = AttemptContextReactiveAccessor.getState(atr); + return state != null ? state : AttemptState.NOT_STARTED; + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionx.java similarity index 95% rename from src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java rename to src/main/java/org/springframework/data/couchbase/transaction/ClientSessionx.java index ca968753a..a09b8e0f6 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSession.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionx.java @@ -18,7 +18,7 @@ * @author Michael Reiche */ // todo gp understand why this is needed -public interface ClientSession /*extends com.mongodb.session.ClientSession*/ { +public interface ClientSessionx /*extends com.mongodb.session.ClientSession*/ { Mono getScope(); diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java index ee9facf31..4125ef42c 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java @@ -59,7 +59,7 @@ public ACRInvocationHandler(/*ReactiveTransactionAttemptContext acr,*/ Transacti } public ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template) { - ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) txOperator) + ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseTransactionalOperator) txOperator) .getTransactionManager()); if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { throw new CouchbaseException( @@ -75,7 +75,7 @@ public > R repository(R repo) { throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo); } ReactiveCouchbaseOperations reactiveOperations = (ReactiveCouchbaseOperations) repo.getOperations(); - ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) txOperator) + ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseTransactionalOperator) txOperator) .getTransactionManager()); if (reactiveOperations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { @@ -83,7 +83,7 @@ public > R repository(R repo) { "Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + repo); } - return repo.withTransaction((CouchbaseStuffHandle) txOperator); // this returns a new repository proxy with txOperator in its threadLocal + return repo.withTransaction((CouchbaseTransactionalOperator) txOperator); // this returns a new repository proxy with txOperator in its threadLocal // what if instead we returned a new repo with a new template with the txOperator? } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java deleted file mode 100644 index 16ab949a7..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolder.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2019-2021 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.transaction; - -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.lang.Nullable; -import org.springframework.transaction.support.ResourceHolderSupport; - -/** - * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveCouchbaseTransactionManager} binds - * instances of this class to the subscriber context. - *

    - * Note: Intended for internal usage only. - * - * @author Mark Paluch - * @author Christoph Strobl - * @since 2.2 - * @see CouchbaseTransactionManager - * @see CouchbaseTemplate - */ -// todo gp understand why this is needed - can we not just hold ctx in Mono context? -public class CouchbaseResourceHolder extends ResourceHolderSupport { - - private @Nullable ClientSession session; // which holds the atr - private CouchbaseClientFactory databaseFactory; - - /** - * Create a new {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} for a given - * {@link ClientSession session}. - * - * @param session the associated {@link ClientSession}. Can be {@literal null}. - * @param databaseFactory the associated {@link CouchbaseClientFactory}. must not be {@literal null}. - */ - public CouchbaseResourceHolder(@Nullable ClientSession session, CouchbaseClientFactory databaseFactory) { - - this.session = session; - this.databaseFactory = databaseFactory; - } - - /** - * @return the associated {@link ClientSession}. Can be {@literal null}. - */ - @Nullable - public ClientSession getSession() { - return session; - } - - /** - * @return the required associated {@link ClientSession}. - * @throws IllegalStateException if no session is associated. - */ - ClientSession getRequiredSession() { - - ClientSession session = getSession(); - - if (session == null) { - throw new IllegalStateException("No ClientSession associated"); - } - return session; - } - - /** - * @return the associated {@link CouchbaseClientFactory}. - */ - public CouchbaseClientFactory getDatabaseFactory() { - return databaseFactory; - } - - /** - * Set the {@link ClientSession} to guard. - * - * @param session can be {@literal null}. - */ - public void setSession(@Nullable ClientSession session) { - this.session = session; - } - - /** - * @return {@literal true} if session is not {@literal null}. - */ - boolean hasSession() { - return session != null; - } - - /** - * If the {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} is - * {@link #hasSession() not already associated} with a {@link ClientSession} the given value is - * {@link #setSession(ClientSession) set} and returned, otherwise the current bound session is returned. - * - * @param session - * @return - */ - @Nullable - public ClientSession setSessionIfAbsent(@Nullable ClientSession session) { - - if (!hasSession()) { - setSession(session); - } - - return session; - } - - /** - * @return {@literal true} if the session is active and has not been closed. - */ - boolean hasActiveSession() { - - if (!hasSession()) { - return false; - } - - return hasServerSession() && !getRequiredSession().getServerSession().isClosed(); - } - - /** - * @return {@literal true} if the session has an active transaction. - * @see #hasActiveSession() - */ - boolean hasActiveTransaction() { - - if (!hasActiveSession()) { - return false; - } - - return getRequiredSession().hasActiveTransaction(); - } - - /** - * @return {@literal true} if the {@link ClientSession} has a {link com.mongodb.session.ServerSession} associated that - * is accessible via {@link ClientSession#getServerSession()}. - */ - boolean hasServerSession() { - - try { - return getRequiredSession().getServerSession() != null; - } catch (IllegalStateException serverSessionClosed) { - // ignore - } - - return false; - } -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolderx.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolderx.java new file mode 100644 index 000000000..a4ba04574 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolderx.java @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2021 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.transaction; + +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; + +/** + * MongoDB specific resource holder, wrapping a {@link CoreTransactionAttemptContext}. + * {@link ReactiveCouchbaseTransactionManager} binds instances of this class to the subscriber context. + *

    + * Note: Intended for internal usage only. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.2 + * @see CouchbaseTransactionManager + * @see CouchbaseTemplate + */ +// todo gp understand why this is needed - can we not just hold ctx in Mono context? +public class CouchbaseResourceHolderx extends ResourceHolderSupport { + + private @Nullable CoreTransactionAttemptContext core; // which holds the atr + private CouchbaseClientFactory databaseFactory; + + /** + * Create a new {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} for a given + * {@link CoreTransactionAttemptContext session}. + * + * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + * @param databaseFactory the associated {@link CouchbaseClientFactory}. must not be {@literal null}. + */ + public CouchbaseResourceHolderx(@Nullable CoreTransactionAttemptContext core, CouchbaseClientFactory databaseFactory) { + + this.core = core; + this.databaseFactory = databaseFactory; + } + + /** + * @return the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + @Nullable + public CoreTransactionAttemptContext getCore() { + return core; + } + + /** + * @return the required associated {@link CoreTransactionAttemptContext}. + * @throws IllegalStateException if no session is associated. + */ + CoreTransactionAttemptContext getRequiredSession() { + + CoreTransactionAttemptContext session = getCore(); + + if (session == null) { + throw new IllegalStateException("No ClientSession associated"); + } + return session; + } + + /** + * @return the associated {@link CouchbaseClientFactory}. + */ + public CouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + /** + * Set the {@link CoreTransactionAttemptContext} to guard. + * + * @param core can be {@literal null}. + */ + public void setCore(@Nullable CoreTransactionAttemptContext core) { + this.core = core; + } + + /** + * @return {@literal true} if session is not {@literal null}. + */ + boolean hasCore() { + return core != null; + } + + /** + * If the {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} is {@link #hasCore()} + * not already associated} with a {@link CoreTransactionAttemptContext} the given value is + * {@link #setCore(CoreTransactionAttemptContext) set} and returned, otherwise the current bound session is returned. + * + * @param core + * @return + */ + @Nullable + public CoreTransactionAttemptContext setSessionIfAbsent(@Nullable CoreTransactionAttemptContext core) { + + if (!hasCore()) { + setCore(core); + } + + return core; + } + +} 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 0261006ad..a8c0434d0 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java @@ -15,34 +15,42 @@ */ package org.springframework.data.couchbase.transaction; +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.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.util.concurrent.atomic.AtomicReference; // todo gp experimenting with simplest possible CallbackPreferringPlatformTransactionManager, extending PlatformTransactionManager // not AbstractPlatformTransactionManager -public class CouchbaseSimpleCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager { +public class CouchbaseSimpleCallbackTransactionManager /* extends AbstractPlatformTransactionManager*/ implements CallbackPreferringPlatformTransactionManager { private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); - private final CouchbaseClientFactory couchbaseClientFactory; + private final ReactiveCouchbaseClientFactory couchbaseClientFactory; private TransactionOptions options; - public CouchbaseSimpleCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory, TransactionOptions options) { + public CouchbaseSimpleCallbackTransactionManager(ReactiveCouchbaseClientFactory couchbaseClientFactory, TransactionOptions options) { this.couchbaseClientFactory = couchbaseClientFactory; this.options = options; } @@ -51,7 +59,7 @@ public CouchbaseSimpleCallbackTransactionManager(CouchbaseClientFactory couchbas public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { final AtomicReference execResult = new AtomicReference<>(); - couchbaseClientFactory.getCluster().transactions().run(ctx -> { + couchbaseClientFactory.getCluster().block().transactions().run(ctx -> { CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(null, true, false, false, true, null, null); // Setting ThreadLocal storage @@ -60,6 +68,11 @@ public T execute(TransactionDefinition definition, TransactionCallback ca TransactionSynchronizationManager.unbindResourceIfPossible(TransactionAttemptContext.class); TransactionSynchronizationManager.bindResource(TransactionAttemptContext.class, ctx); + + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); + TransactionSynchronizationManager.unbindResourceIfPossible(couchbaseClientFactory.getCluster().block()); + TransactionSynchronizationManager.bindResource(couchbaseClientFactory.getCluster().block(), resourceHolder); + try { execResult.set(callback.doInTransaction(status)); } @@ -73,18 +86,29 @@ public T execute(TransactionDefinition definition, TransactionCallback ca return execResult.get(); } + /** + * Test transaction infrastructure uses this to determine if transaction is active + * + * @param definition + * @return + * @throws TransactionException + */ @Override - public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { - return null; + public TransactionStatus getTransaction(@Nullable TransactionDefinition definition) + throws TransactionException { + TransactionStatus status = new DefaultTransactionStatus( null, true, true, + false, true, false); + return status; } @Override public void commit(TransactionStatus status) throws TransactionException { - System.out.println("commit"); + LOGGER.debug("NO-OP: Committing Couchbase Transaction with status {}", status); } @Override public void rollback(TransactionStatus status) throws TransactionException { - System.out.println("rollback"); + LOGGER.warn("NO-OP: Rolling back Couchbase Transaction with status {}", status); } + } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java deleted file mode 100644 index 14f0d441a..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseStuffHandle.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.springframework.data.couchbase.transaction; - -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import com.couchbase.client.java.transactions.TransactionGetResult; -import com.couchbase.client.java.transactions.TransactionResult; -import org.springframework.lang.Nullable; -import org.springframework.transaction.support.TransactionSynchronizationManager; -import reactor.core.publisher.Mono; - -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.repository.DynamicProxyable; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.transaction.ReactiveTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.util.Assert; - -import com.couchbase.client.core.error.CouchbaseException; - -public class CouchbaseStuffHandle { - - // package org.springframework.transaction.reactive; - private static final Log logger = LogFactory.getLog(CouchbaseStuffHandle.class); - private final ReactiveTransactionManager transactionManager; - private final TransactionDefinition transactionDefinition; - - Map getResultMap = new HashMap<>(); - private ReactiveTransactionAttemptContext attemptContextReactive; - - public CouchbaseStuffHandle() { - transactionManager = null; - transactionDefinition = null; - } - - public CouchbaseStuffHandle(ReactiveCouchbaseTransactionManager transactionManager) { - this(transactionManager, new CouchbaseTransactionDefinition()); - } - - public CouchbaseStuffHandle(ReactiveCouchbaseTransactionManager transactionManager, - TransactionDefinition transactionDefinition) { - Assert.notNull(transactionManager, "ReactiveTransactionManager must not be null"); - Assert.notNull(transactionDefinition, "TransactionDefinition must not be null"); - this.transactionManager = transactionManager; - this.transactionDefinition = transactionDefinition; - } - - public Mono reactive(Function> transactionLogic) { - return reactive(transactionLogic, true); - } - - /** - * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default - * PerTransactionConfig. - */ - public Mono reactive(Function> transactionLogic, - boolean commit) { - // todo gp this needs access to a Cluster - return Mono.empty(); -// return ((ReactiveCouchbaseTransactionManager) transactionManager).getTransactions().reactive((ctx) -> { -// setAttemptContextReactive(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport -// // for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method -// // and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder() -// return transactionLogic.apply(this); -// }/*, commit*/); - } - - public TransactionResultHolder transactionResultHolder(Integer key) { - return getResultMap.get(key); - } - - public TransactionResultHolder transactionResultHolder(TransactionGetResult result) { - TransactionResultHolder holder = new TransactionResultHolder(result); - getResultMap.put(System.identityHashCode(holder), holder); - return holder; - } - - public void setAttemptContextReactive(ReactiveTransactionAttemptContext attemptContextReactive) { - this.attemptContextReactive = attemptContextReactive; - // see ReactiveCouchbaseTransactionManager.doBegin() - // transactionManager.getReactiveTransaction(new CouchbaseTransactionDefinition()).block(); - // CouchbaseResourceHolder holder = null; - //TransactionSynchronizationManager.bindResource(((ReactiveCouchbaseTransactionManager)transactionManager).getDatabaseFactory(), holder); - - /* - for savePerson that, doBegin() is called from AbstractReactiveTransactionManager.getReactiveTransaction() - which is called from TransactionalOperatorImpl.transactional(Mono) - [also called from TransactionalOperatorImpl.execute(TransactionCallback)] - */ - } - - public ReactiveTransactionAttemptContext getAttemptContextReactive() { - return attemptContextReactive; - } - - public ReactiveTransactionManager getTransactionManager() { - return transactionManager; - } - - public ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template) { - ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) this) - .getTransactionManager()); - if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { - throw new CouchbaseException( - "Template must use the same clientFactory as the transactionManager of the transactionalOperator " - + template); - } - return template;// .with(this); // template with a new couchbaseClient with txOperator - } - - /* - public CouchbaseTemplate template(CouchbaseTemplate template) { - CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) ((CouchbaseStuffHandle) this) - .getTransactionManager()); - if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { - throw new CouchbaseException( - "Template must use the same clientFactory as the transactionManager of the transactionalOperator " - + template); - } - return template.with(this); // template with a new couchbaseClient with txOperator - } -*/ - public > R repository(R repo) { - if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) { - throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo); - } - ReactiveCouchbaseOperations reactiveOperations = (ReactiveCouchbaseOperations) repo.getOperations(); - ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseStuffHandle) this) - .getTransactionManager()); - - if (reactiveOperations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { - throw new CouchbaseException( - "Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + repo); - } - return repo.withTransaction(this); // this returns a new repository proxy with txOperator in its threadLocal - // what if instead we returned a new repo with a new template with the txOperator? - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java index e9fc558a3..a1490e5dc 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java @@ -16,7 +16,10 @@ package org.springframework.data.couchbase.transaction; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.Transactions; import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -38,13 +41,13 @@ /** * A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages - * {@link ClientSession} based transactions for a single {@link CouchbaseClientFactory}. + * {@link CoreTransactionAttemptContext} based transactions for a single {@link CouchbaseClientFactory}. *

    - * Binds a {@link ClientSession} from the specified {@link CouchbaseClientFactory} to the thread. + * Binds a {@link CoreTransactionAttemptContext} from the specified {@link CouchbaseClientFactory} to the thread. *

    - * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal - * consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction() - * commit} or {@link ClientSession#abortTransaction() abort} a transaction. + * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link CoreTransactionAttemptContext} and enable causal + * consistency, and also {@link CoreTransactionAttemptContext#startTransaction() start}, {@link CoreTransactionAttemptContext#commitTransaction() + * commit} or {@link CoreTransactionAttemptContext#abortTransaction() abort} a transaction. *

    * TODO: Application code is required to retrieve the {@link com.couchbase.client.java.Cluster} ????? via * {@link ?????#getDatabase(CouchbaseClientFactory)} instead of a standard {@link CouchbaseClientFactory#getCluster()} @@ -66,6 +69,7 @@ public class CouchbaseTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { + private Transactions transactions; private @Nullable CouchbaseClientFactory databaseFactory; private @Nullable TransactionOptions options; @@ -90,12 +94,14 @@ public CouchbaseTransactionManager() {} * * @param databaseFactory must not be {@literal null}. @//param options can be {@literal null}. */ - public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory) { + public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory, @Nullable TransactionOptions options) { Assert.notNull(databaseFactory, "DbFactory must not be null!"); System.err.println(this); System.err.println(databaseFactory.getCluster()); this.databaseFactory = databaseFactory; + this.options = options; + this.transactions = databaseFactory.getCluster().transactions(); } /* @@ -104,8 +110,7 @@ public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory) { */ @Override protected Object doGetTransaction() throws TransactionException { - - CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager + ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager .getResource(getRequiredDatabaseFactory().getCluster()); return new CouchbaseTransactionObject(resourceHolder); } @@ -128,28 +133,33 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); // should ACR already be in TSM? TransactionSynchronizationManager.bindResource(getRequiredDbFactory().getCluster(), resourceHolder); - CouchbaseResourceHolder resourceHolder = newResourceHolder(definition, - ClientSessionOptions.builder().causallyConsistent(true).build(), + ReactiveCouchbaseResourceHolder resourceHolder = newResourceHolder(definition, TransactionOptions.transactionOptions(), null /* ((CouchbaseTransactionDefinition) definition).getAttemptContextReactive()*/); couchbaseTransactionObject.setResourceHolder(resourceHolder); if (logger.isDebugEnabled()) { logger - .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession()))); + .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); } try { couchbaseTransactionObject.startTransaction(options); } catch (CouchbaseException ex) { throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.", - debugString(couchbaseTransactionObject.getSession())), ex); + debugString(couchbaseTransactionObject.getCore())), ex); } if (logger.isDebugEnabled()) { - logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession()))); + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getCore()))); } + TransactionSynchronizationManager.setActualTransactionActive(true); + // use the ResourceHolder which contains the core + //TransactionSynchronizationManager.unbindResourceIfPossible(TransactionAttemptContext.class); + //TransactionSynchronizationManager.bindResource(CoreTransactionAttemptContext.class, resourceHolder.getCore()); + resourceHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.unbindResourceIfPossible( getRequiredDatabaseFactory().getCluster()); System.err.println("CouchbaseTransactionManager: "+this); System.err.println("bindResource: "+ getRequiredDatabaseFactory().getCluster()+" value: "+resourceHolder); TransactionSynchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster(), resourceHolder); @@ -188,15 +198,15 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio if (logger.isDebugEnabled()) { logger.debug(String.format("About to commit transaction for session %s.", - debugString(couchbaseTransactionObject.getSession()))); + debugString(couchbaseTransactionObject.getCore()))); } try { doCommit(couchbaseTransactionObject); } catch (Exception ex) { - logger.debug("could not commit Couchbase transaction for session "+debugString(couchbaseTransactionObject.getSession())); + logger.debug("could not commit Couchbase transaction for session "+debugString(couchbaseTransactionObject.getCore())); throw new TransactionSystemException(String.format("Could not commit Couchbase transaction for session %s.", - debugString(couchbaseTransactionObject.getSession())), ex); + debugString(couchbaseTransactionObject.getCore())), ex); } } @@ -205,7 +215,7 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio * If a commit operation encounters an error, the MongoDB driver throws a {@link CouchbaseException} holding * {@literal error labels}.
    * By default those labels are ignored, nevertheless one might check for - * {@link CouchbaseException#UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL transient commit errors labels} and retry the the + * {@link CouchbaseException transient commit errors labels} and retry the the * commit.
    * *

    @@ -242,7 +252,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc
     
     		if (logger.isDebugEnabled()) {
     			logger.debug(String.format("About to abort transaction for session %s.",
    -					debugString(couchbaseTransactionObject.getSession())));
    +					debugString(couchbaseTransactionObject.getCore())));
     		}
     
     		try {
    @@ -250,7 +260,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc
     		} catch (CouchbaseException ex) {
     
     			throw new TransactionSystemException(String.format("Could not abort Couchbase transaction for session %s.",
    -					debugString(couchbaseTransactionObject.getSession())), ex);
    +					debugString(couchbaseTransactionObject.getCore())), ex);
     		}
     	}
     
    @@ -262,7 +272,8 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc
     	protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException {
     
     		CouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status);
    -		transactionObject.getRequiredResourceHolder().setRollbackOnly();
    +		throw new TransactionException("need to setRollbackOnly() here"){};
    +		//transactionObject.getRequiredResourceHolder().setRollbackOnly();
     	}
     
     	/*
    @@ -279,12 +290,12 @@ protected void doCleanupAfterCompletion(Object transaction) {
     		CouchbaseTransactionObject couchbaseTransactionObject = (CouchbaseTransactionObject) transaction;
     
     		// Remove the connection holder from the thread.
    -		TransactionSynchronizationManager.unbindResource(getRequiredDatabaseFactory().getCluster());
    -		couchbaseTransactionObject.getRequiredResourceHolder().clear();
    +		TransactionSynchronizationManager.unbindResourceIfPossible(getRequiredDatabaseFactory().getCluster());
    +		//couchbaseTransactionObject.getRequiredResourceHolder().clear();
     
     		if (logger.isDebugEnabled()) {
    -			logger.debug(String.format("About to release Session %s after transaction.",
    -					debugString(couchbaseTransactionObject.getSession())));
    +			logger.debug(String.format("About to release Core %s after transaction.",
    +					debugString(couchbaseTransactionObject.getCore())));
     		}
     
     		couchbaseTransactionObject.closeSession();
    @@ -338,13 +349,13 @@ public void afterPropertiesSet() {
     		getRequiredDatabaseFactory();
     	}
     
    -	private CouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options,
    -			ReactiveTransactionAttemptContext atr) {
    +	private ReactiveCouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, TransactionOptions options,
    +			CoreTransactionAttemptContext atr) {
     
     		CouchbaseClientFactory databaseFactory = getResourceFactory();
     
    -		CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(
    -				databaseFactory.getSession(options, atr), databaseFactory);
    +		ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(
    +				databaseFactory.getCore(options, atr));
     		// TODO resourceHolder.setTimeoutIfNotDefaulted(determineTimeout(definition));
     
     		return resourceHolder;
    @@ -379,7 +390,7 @@ private static CouchbaseTransactionObject extractCouchbaseTransaction(DefaultTra
     		return (CouchbaseTransactionObject) status.getTransaction();
     	}
     
    -	private static String debugString(@Nullable ClientSession session) {
    +	private static String debugString(@Nullable CoreTransactionAttemptContext session) {
     
     		if (session == null) {
     			return "null";
    @@ -389,19 +400,7 @@ private static String debugString(@Nullable ClientSession session) {
     				Integer.toHexString(session.hashCode()));
     
     		try {
    -			if (session.getServerSession() != null) {
    -				debugString += String.format("id = %s, ", session.getServerSession().getIdentifier());
    -				debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
    -				debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
    -				debugString += String.format("txNumber = %d, ", session.getServerSession().getTransactionNumber());
    -				debugString += String.format("closed = %d, ", session.getServerSession().isClosed());
    -				debugString += String.format("clusterTime = %s", session.getClusterTime());
    -			} else {
    -				debugString += "id = n/a";
    -				debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
    -				debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
    -				debugString += String.format("clusterTime = %s", session.getClusterTime());
    -			}
    +			debugString += String.format("core=%s",session);
     		} catch (RuntimeException e) {
     			debugString += String.format("error = %s", e.getMessage());
     		}
    @@ -416,33 +415,33 @@ public CouchbaseClientFactory getDatabaseFactory() {
     	}
     
     	/**
    -	 * MongoDB specific transaction object, representing a {@link CouchbaseResourceHolder}. Used as transaction object by
    +	 * MongoDB specific transaction object, representing a {@link ReactiveCouchbaseResourceHolder}. Used as transaction object by
     	 * {@link CouchbaseTransactionManager}.
     	 *
     	 * @author Christoph Strobl
     	 * @author Mark Paluch
     	 * @since 2.1
    -	 * @see CouchbaseResourceHolder
    +	 * @see ReactiveCouchbaseResourceHolder
     	 */
     	protected static class CouchbaseTransactionObject implements SmartTransactionObject {
     
    -		private @Nullable CouchbaseResourceHolder resourceHolder;
    +		private @Nullable ReactiveCouchbaseResourceHolder resourceHolder;
     
    -		CouchbaseTransactionObject(@Nullable CouchbaseResourceHolder resourceHolder) {
    +		CouchbaseTransactionObject(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) {
     			this.resourceHolder = resourceHolder;
     		}
     
     		/**
    -		 * Set the {@link CouchbaseResourceHolder}.
    +		 * Set the {@link ReactiveCouchbaseResourceHolder}.
     		 *
     		 * @param resourceHolder can be {@literal null}.
     		 */
    -		void setResourceHolder(@Nullable CouchbaseResourceHolder resourceHolder) {
    +		void setResourceHolder(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) {
     			this.resourceHolder = resourceHolder;
     		}
     
     		/**
    -		 * @return {@literal true} if a {@link CouchbaseResourceHolder} is set.
    +		 * @return {@literal true} if a {@link ReactiveCouchbaseResourceHolder} is set.
     		 */
     		final boolean hasResourceHolder() {
     			return resourceHolder != null;
    @@ -455,11 +454,11 @@ final boolean hasResourceHolder() {
     		 */
     		void startTransaction(TransactionOptions options) {
     
    -			ClientSession session = getRequiredSession();
    +			CoreTransactionAttemptContext core = getRequiredCore();
     			// if (options != null) {
     			// session.startTransaction(options);
     			// } else {
    -			session.startTransaction();
    +			//core.startTransaction();
     			// }
     		}
     
    @@ -467,43 +466,37 @@ void startTransaction(TransactionOptions options) {
     		 * Commit the transaction.
     		 */
     		public void commitTransaction() {
    -			((Mono)(getRequiredSession().commitTransaction())).block();
    +			getRequiredCore().commit().block();
     		}
     
     		/**
     		 * Rollback (abort) the transaction.
     		 */
     		public void abortTransaction() {
    -			((Mono)(getRequiredSession().abortTransaction())).block();
    +			getRequiredCore().rollback().block();
     		}
     
     		/**
    -		 * Close a {@link ClientSession} without regard to its transactional state.
    +		 * Close a {@link CoreTransactionAttemptContext} without regard to its transactional state.
     		 */
     		void closeSession() {
    -
    -			ClientSession session = getRequiredSession();
    -			if (session.getServerSession() != null && !session.getServerSession().isClosed()) {
    -				session.close();
    -			}
    +			CoreTransactionAttemptContext core = getRequiredCore();
     		}
     
     		@Nullable
    -		public ClientSession getSession() {
    -			return resourceHolder != null ? resourceHolder.getSession() : null;
    +		public CoreTransactionAttemptContext getCore() {
    +			return resourceHolder != null ? resourceHolder.getCore() : null;
     		}
     
    -		private CouchbaseResourceHolder getRequiredResourceHolder() {
    -
    +		private ReactiveCouchbaseResourceHolder getRequiredResourceHolder() {
     			Assert.state(resourceHolder != null, "CouchbaseResourceHolder is required but not present. o_O");
     			return resourceHolder;
     		}
     
    -		private ClientSession getRequiredSession() {
    -
    -			ClientSession session = getSession();
    -			Assert.state(session != null, "A Session is required but it turned out to be null.");
    -			return session;
    +		private CoreTransactionAttemptContext getRequiredCore() {
    +			CoreTransactionAttemptContext core = getCore();
    +			Assert.state(core != null, "A Core is required but it turned out to be null.");
    +			return core;
     		}
     
     		/*
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java
    new file mode 100644
    index 000000000..12e967ea0
    --- /dev/null
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java
    @@ -0,0 +1,231 @@
    +package org.springframework.data.couchbase.transaction;
    +
    +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
    +import com.couchbase.client.core.transaction.CoreTransactionGetResult;
    +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor;
    +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext;
    +import com.couchbase.client.java.transactions.TransactionGetResult;
    +import com.couchbase.client.java.transactions.TransactionResult;
    +import org.springframework.data.couchbase.core.CouchbaseTemplate;
    +import org.springframework.transaction.ReactiveTransaction;
    +import org.springframework.transaction.TransactionException;
    +import org.springframework.transaction.TransactionSystemException;
    +import org.springframework.transaction.reactive.TransactionCallback;
    +import org.springframework.transaction.reactive.TransactionContextManager;
    +import org.springframework.transaction.reactive.TransactionalOperator;
    +import reactor.core.publisher.Flux;
    +import reactor.core.publisher.Mono;
    +
    +import java.util.HashMap;
    +import java.util.Map;
    +import java.util.function.Function;
    +
    +import org.apache.commons.logging.Log;
    +import org.apache.commons.logging.LogFactory;
    +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
    +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
    +import org.springframework.data.couchbase.repository.DynamicProxyable;
    +import org.springframework.data.couchbase.repository.support.TransactionResultHolder;
    +import org.springframework.transaction.ReactiveTransactionManager;
    +import org.springframework.transaction.TransactionDefinition;
    +import org.springframework.util.Assert;
    +
    +import com.couchbase.client.core.error.CouchbaseException;
    +
    +/**
    + * What's this for again?
    + * A transaction-enabled operator that uses the CouchbaseStuffHandle txOp instead of
    + * what it finds in the currentContext()?
    + *
    + */
    +public class CouchbaseTransactionalOperator implements TransactionalOperator {
    +
    +	// package org.springframework.transaction.reactive;
    +	private static final Log logger = LogFactory.getLog(CouchbaseTransactionalOperator.class);
    +	private final ReactiveTransactionManager transactionManager;
    +	private final TransactionDefinition transactionDefinition;
    +
    +	Map getResultMap = new HashMap<>();
    +	private ReactiveTransactionAttemptContext attemptContextReactive;
    +
    +	public CouchbaseTransactionalOperator() {
    +		transactionManager = null;
    +		transactionDefinition = null;
    +	}
    +
    +	public CouchbaseTransactionalOperator(ReactiveCouchbaseTransactionManager transactionManager) {
    +		this(transactionManager, new CouchbaseTransactionDefinition());
    +	}
    +
    +	public ReactiveCouchbaseTemplate getTemplate(){
    +		return ((ReactiveCouchbaseTransactionManager)transactionManager).getDatabaseFactory().getTransactionalOperator()
    +				.getTemplate();
    +	}
    +	public CouchbaseTransactionalOperator(ReactiveCouchbaseTransactionManager transactionManager,
    +																				TransactionDefinition transactionDefinition) {
    +		Assert.notNull(transactionManager, "ReactiveTransactionManager must not be null");
    +		Assert.notNull(transactionDefinition, "TransactionDefinition must not be null");
    +		this.transactionManager = transactionManager;
    +		this.transactionDefinition = transactionDefinition;
    +	}
    +
    +	public Mono reactive(Function> transactionLogic) {
    +		return reactive(transactionLogic, true);
    +	}
    +
    +	public TransactionResult run(Function transactionLogic) {
    +		return reactive(new Function>() {
    +											@Override
    +											public Mono apply(CouchbaseTransactionalOperator couchbaseTransactionalOperator) {
    +												return Mono.defer(() -> {transactionLogic.apply( couchbaseTransactionalOperator); return Mono.empty();});
    +											}
    +										},
    +				true).block();
    +	}
    +
    +
    +	/**
    +	 * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default
    +	 * PerTransactionConfig.
    +	 */
    +	public Mono reactive(Function> transactionLogic,
    +											boolean commit) {
    +//		// todo gp this needs access to a Cluster
    +//		return Mono.empty();
    +		return ((ReactiveCouchbaseTransactionManager) transactionManager).getDatabaseFactory().getCluster().block().reactive().transactions().run(ctx -> {
    +			setAttemptContextReactive(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport
    +			// for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method
    +			// and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder()
    +			return transactionLogic.apply(this);
    +		}/*, commit*/);
    +	}
    +
    +	public TransactionResultHolder transactionResultHolder(Integer key) {
    +		return getResultMap.get(key);
    +	}
    +
    +	public TransactionResultHolder transactionResultHolder(CoreTransactionGetResult result) {
    +		TransactionResultHolder holder = new TransactionResultHolder(result);
    +		getResultMap.put(System.identityHashCode(holder), holder);
    +		return holder;
    +	}
    +
    +	public void setAttemptContextReactive(ReactiveTransactionAttemptContext attemptContextReactive) {
    +		this.attemptContextReactive = attemptContextReactive;
    +		// see ReactiveCouchbaseTransactionManager.doBegin()
    +		// transactionManager.getReactiveTransaction(new CouchbaseTransactionDefinition()).block();
    +	//	CouchbaseResourceHolder holder = null;
    +	//TransactionSynchronizationManager.bindResource(((ReactiveCouchbaseTransactionManager)transactionManager).getDatabaseFactory(), holder);
    +
    +		/*
    +		for savePerson that,  doBegin() is called from AbstractReactiveTransactionManager.getReactiveTransaction()
    +		which is called from TransactionalOperatorImpl.transactional(Mono)
    +		[also called from TransactionalOperatorImpl.execute(TransactionCallback)]
    +		 */
    +	}
    +
    +	public ReactiveTransactionAttemptContext getAttemptContextReactive() {
    +		return attemptContextReactive;
    +	}
    +
    +	public CoreTransactionAttemptContext getAttemptContext() {
    +		return AttemptContextReactiveAccessor.getCore(attemptContextReactive);
    +	}
    +
    +
    +	public ReactiveTransactionManager getTransactionManager() {
    +		return transactionManager;
    +	}
    +
    +	public ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template) {
    +		ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseTransactionalOperator) this)
    +				.getTransactionManager());
    +		if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) {
    +			throw new CouchbaseException(
    +					"Template must use the same clientFactory as the transactionManager of the transactionalOperator "
    +							+ template);
    +		}
    +		return template.with(this); // template with a new couchbaseClient with txOperator
    +	}
    +
    +	/*
    +	public CouchbaseTemplate template(CouchbaseTemplate template) {
    +		CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) ((CouchbaseStuffHandle) this)
    +				.getTransactionManager());
    +		if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) {
    +			throw new CouchbaseException(
    +					"Template must use the same clientFactory as the transactionManager of the transactionalOperator "
    +							+ template);
    +		}
    +		return template.with(this); // template with a new couchbaseClient with txOperator
    +	}
    +*/
    +	public > R repository(R repo) {
    +		if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) {
    +			throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo);
    +		}
    +		ReactiveCouchbaseOperations reactiveOperations = (ReactiveCouchbaseOperations) repo.getOperations();
    +		ReactiveCouchbaseTransactionManager txMgr = (ReactiveCouchbaseTransactionManager) this.getTransactionManager();
    +
    +		if (reactiveOperations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) {
    +			throw new CouchbaseException(
    +					"Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + repo);
    +		}
    +		return repo.withTransaction(this); // this returns a new repository proxy with txOperator in its threadLocal
    +		// what if instead we returned a new repo with a new template with the txOperator?
    +	}
    +
    +	@Override
    +	public  Flux transactional(Flux flux) {
    +		return execute(it -> flux);
    +	}
    +
    +	@Override
    +	public  Mono transactional(Mono mono) {
    +		return TransactionContextManager.currentContext().flatMap(context -> {
    +			// getCtx()/getAttemptTransActionReactive() has the atr
    +			// atr : transactionalOpterator -> transactionDefinition -> transactionHolder ->
    +			((CouchbaseTransactionDefinition) transactionDefinition).setAttemptContextReactive(getAttemptContextReactive());
    +			Mono status = this.transactionManager.getReactiveTransaction(this.transactionDefinition);
    +			// This is an around advice: Invoke the next interceptor in the chain.
    +			// This will normally result in a target object being invoked.
    +			// Need re-wrapping of ReactiveTransaction until we get hold of the exception
    +			// through usingWhen.
    +			return status
    +					.flatMap(it -> Mono
    +							.usingWhen(Mono.just(it), ignore -> mono, this.transactionManager::commit, (res, err) -> Mono.empty(),
    +									this.transactionManager::rollback)
    +							.onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex))));
    +		}).contextWrite(TransactionContextManager.getOrCreateContext())
    +				.contextWrite(TransactionContextManager.getOrCreateContextHolder());
    +	}
    +
    +	@Override
    +	public  Flux execute(TransactionCallback action) throws TransactionException {
    +		return TransactionContextManager.currentContext().flatMapMany(context -> {
    +			Mono status = this.transactionManager.getReactiveTransaction(this.transactionDefinition);
    +			// This is an around advice: Invoke the next interceptor in the chain.
    +			// This will normally result in a target object being invoked.
    +			// Need re-wrapping of ReactiveTransaction until we get hold of the exception
    +			// through usingWhen.
    +			return status
    +					.flatMapMany(it -> Flux
    +							.usingWhen(Mono.just(it), action::doInTransaction, this.transactionManager::commit,
    +									(tx, ex) -> Mono.empty(), this.transactionManager::rollback)
    +							.onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex))));
    +		}).contextWrite(TransactionContextManager.getOrCreateContext())
    +				.contextWrite(TransactionContextManager.getOrCreateContextHolder());
    +	}
    +
    +	private Mono rollbackOnException(ReactiveTransaction status, Throwable ex) throws TransactionException {
    +		logger.debug("Initiating transaction rollback on application exception", ex);
    +		return this.transactionManager.rollback(status).onErrorMap((ex2) -> {
    +			logger.error("Application exception overridden by rollback exception", ex);
    +			if (ex2 instanceof TransactionSystemException) {
    +				((TransactionSystemException) ex2).initApplicationException(ex);
    +			}
    +			return ex2;
    +		});
    +	}
    +
    +}
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java
    index 2115f5985..f4be7d8f8 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java
    @@ -1,6 +1,8 @@
     package org.springframework.data.couchbase.transaction;
     
    +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
     import com.couchbase.client.java.ClusterInterface;
    +import com.couchbase.client.java.transactions.config.TransactionOptions;
     import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory;
     import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
     import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
    @@ -120,7 +122,7 @@ private static Mono doGetCouchbaseCluster(@Nullable String dbN
     				.flatMap(synchronizationManager -> {
     
     					return doGetSession(synchronizationManager, factory, sessionSynchronization) //
    -							.flatMap(it -> getCouchbaseClusterOrDefault(dbName, factory.withSession(it)));
    +							.flatMap(it -> getCouchbaseClusterOrDefault(dbName, factory.withCore(it)));
     				}) //
     				.onErrorResume(NoTransactionException.class, e -> getCouchbaseClusterOrDefault(dbName, factory)) // hitting this
     				.switchIfEmpty(getCouchbaseClusterOrDefault(dbName, factory));
    @@ -140,28 +142,27 @@ private static Mono doGetCouchbaseTemplate(@Nullable
     		//		.getResource(factory);
     
     		return TransactionSynchronizationManager.forCurrentTransaction()
    -				.flatMap(x -> {return Mono.just(x);})
     				.filter(TransactionSynchronizationManager::isSynchronizationActive) //
     				.flatMap(synchronizationManager -> {
     					return doGetSession(synchronizationManager, factory, sessionSynchronization) //
    -							.flatMap(it -> getCouchbaseTemplateOrDefault(dbName, factory.withSession(it), converter)); // rx TxMgr
    +							.flatMap(it -> getCouchbaseTemplateOrDefault(dbName, factory.withCore(it), converter)); // rx TxMgr
     				}) //
     				.onErrorResume(NoTransactionException.class,
     						e -> { return getCouchbaseTemplateOrDefault(dbName,
    -								getNonReactiveSession(factory) != null ? factory.withSession(getNonReactiveSession(factory)) : factory,
    +								getNonReactiveSession(factory) != null ? factory.withCore(getNonReactiveSession(factory)) : factory,
     								converter);}) // blocking TxMgr
     				.switchIfEmpty(getCouchbaseTemplateOrDefault(dbName, factory, converter));
     	}
     
    -	private static ClientSession getNonReactiveSession(ReactiveCouchbaseClientFactory factory) {
    -		CouchbaseResourceHolder h = ((CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager
    +	private static ReactiveCouchbaseResourceHolder getNonReactiveSession(ReactiveCouchbaseClientFactory factory) {
    +		ReactiveCouchbaseResourceHolder h = ((ReactiveCouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager
     				.getResource(factory.getCluster().block()));
    -		if( h == null){
    -			h = ((CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager
    +		if( h == null){  // no longer used
    +			h = ((ReactiveCouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager
     					.getResource(factory));// MN's CouchbaseTransactionManager
     		}
     		//System.err.println("getNonreactiveSession: "+ h);
    -		return h != null ? h.getSession() : null;
    +		return h;
     	}
     
     	private static Mono getCouchbaseClusterOrDefault(@Nullable String dbName,
    @@ -174,7 +175,7 @@ private static Mono getCouchbaseTemplateOrDefault(@Nu
     		return Mono.just(new ReactiveCouchbaseTemplate(factory, converter));
     	}
     
    -	private static Mono doGetSession(TransactionSynchronizationManager synchronizationManager,
    +	private static Mono doGetSession(TransactionSynchronizationManager synchronizationManager,
     			ReactiveCouchbaseClientFactory dbFactory, SessionSynchronization sessionSynchronization) {
     
     		final ReactiveCouchbaseResourceHolder registeredHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager
    @@ -182,10 +183,12 @@ private static Mono doGetSession(TransactionSynchronizationManage
     
     		// check for native MongoDB transaction
     		if (registeredHolder != null
    -				&& (registeredHolder.hasSession() || registeredHolder.isSynchronizedWithTransaction())) {
    -			System.err.println("doGetSession: got: "+registeredHolder.getSession());
    -			return registeredHolder.hasSession() ? Mono.just(registeredHolder.getSession())
    -					: createClientSession(dbFactory).map(registeredHolder::setSessionIfAbsent);
    +				&& (registeredHolder.hasCore() || registeredHolder.isSynchronizedWithTransaction())) {
    +			System.err.println("doGetSession: got: "+registeredHolder.getCore());
    +			// TODO msr - mabye don't create a session unless it has an atr?
    +			//return registeredHolder.hasCore() ? Mono.just(registeredHolder)
    +			//		: createClientSession(dbFactory).map( core -> { registeredHolder.setCore(core); return registeredHolder;});
    +			return Mono.just(registeredHolder);
     		}
     
     		if (SessionSynchronization.ON_ACTUAL_TRANSACTION.equals(sessionSynchronization)) {
    @@ -198,20 +201,21 @@ private static Mono doGetSession(TransactionSynchronizationManage
     		// init a non native MongoDB transaction by registering a MongoSessionSynchronization
     		return createClientSession(dbFactory).map(session -> {
     
    -			ReactiveCouchbaseResourceHolder newHolder = new ReactiveCouchbaseResourceHolder(session, dbFactory);
    -			newHolder.getRequiredSession().startTransaction();
    +			ReactiveCouchbaseResourceHolder newHolder = new ReactiveCouchbaseResourceHolder(session);
    +			//newHolder.getRequiredCore().startTransaction();
    +			System.err.println(" need to call startTransaction() ");
     
     			synchronizationManager
     					.registerSynchronization(new CouchbaseSessionSynchronization(synchronizationManager, newHolder, dbFactory));
     			newHolder.setSynchronizedWithTransaction(true);
     			synchronizationManager.bindResource(dbFactory, newHolder);
     
    -			return newHolder.getSession();
    +			return newHolder;
     		});
     	}
     
    -	private static Mono createClientSession(ReactiveCouchbaseClientFactory dbFactory) {
    -		return dbFactory.getSession(ClientSessionOptions.builder().causallyConsistent(true).build());
    +	private static Mono createClientSession(ReactiveCouchbaseClientFactory dbFactory) {
    +		return null; // ?? dbFactory.getCore(TransactionOptions.transactionOptions());
     	}
     
     	/**
    @@ -250,7 +254,7 @@ protected boolean shouldReleaseBeforeCompletion() {
     		protected Mono processResourceAfterCommit(ReactiveCouchbaseResourceHolder resourceHolder) {
     
     			if (isTransactionActive(resourceHolder)) {
    -				return Mono.from(resourceHolder.getRequiredSession().commitTransaction());
    +				return Mono.from(resourceHolder.getCore().commit());
     			}
     
     			return Mono.empty();
    @@ -267,7 +271,7 @@ public Mono afterCompletion(int status) {
     
     				if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) {
     
    -					return Mono.from(resourceHolder.getRequiredSession().abortTransaction()) //
    +					return Mono.from(resourceHolder.getCore().rollback()) //
     							.then(super.afterCompletion(status));
     				}
     
    @@ -283,19 +287,19 @@ public Mono afterCompletion(int status) {
     		protected Mono releaseResource(ReactiveCouchbaseResourceHolder resourceHolder, Object resourceKey) {
     
     			return Mono.fromRunnable(() -> {
    -				if (resourceHolder.hasActiveSession()) {
    -					resourceHolder.getRequiredSession().close();
    -				}
    +				//if (resourceHolder.hasActiveSession()) {
    +				//	resourceHolder.getRequiredSession().close();
    +				//}
     			});
     		}
     
     		private boolean isTransactionActive(ReactiveCouchbaseResourceHolder resourceHolder) {
     
    -			if (!resourceHolder.hasSession()) {
    +			if (!resourceHolder.hasCore()) {
     				return false;
     			}
     
    -			return resourceHolder.getRequiredSession().hasActiveTransaction();
    +			return resourceHolder.getRequiredCore() != null;
     		}
     	}
     }
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java
    index d73ba2057..506fb7d26 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java
    @@ -15,15 +15,19 @@
      */
     package org.springframework.data.couchbase.transaction;
     
    -import org.springframework.data.couchbase.CouchbaseClientFactory;
    -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory;
     import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
    +import org.springframework.data.couchbase.repository.support.TransactionResultHolder;
     import org.springframework.lang.Nullable;
     import org.springframework.transaction.support.ResourceHolderSupport;
     
    +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
    +
    +import java.util.HashMap;
    +import java.util.Map;
    +
     /**
    - * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveCouchbaseTransactionManager} binds
    - * instances of this class to the subscriber context.
    + * MongoDB specific resource holder, wrapping a {@link CoreTransactionAttemptContext}.
    + * {@link ReactiveCouchbaseTransactionManager} binds instances of this class to the subscriber context.
      * 

    * Note: Intended for internal usage only. * @@ -36,121 +40,102 @@ // todo gp understand why this is needed public class ReactiveCouchbaseResourceHolder extends ResourceHolderSupport { - private @Nullable ClientSession session; // which holds the atr - private ReactiveCouchbaseClientFactory databaseFactory; + private @Nullable CoreTransactionAttemptContext core; // which holds the atr + Map getResultMap = new HashMap<>(); + + // private ReactiveCouchbaseClientFactory databaseFactory; /** - * Create a new {@link ReactiveCouchbaseResourceHolder} for a given {@link ClientSession session}. + * Create a new {@link ReactiveCouchbaseResourceHolder} for a given {@link CoreTransactionAttemptContext session}. * - * @param session the associated {@link ClientSession}. Can be {@literal null}. - * @param databaseFactory the associated {@link CouchbaseClientFactory}. must not be {@literal null}. + * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. */ - public ReactiveCouchbaseResourceHolder(@Nullable ClientSession session, ReactiveCouchbaseClientFactory databaseFactory) { + public ReactiveCouchbaseResourceHolder(@Nullable CoreTransactionAttemptContext core) { - this.session = session; - this.databaseFactory = databaseFactory; + this.core = core; + // this.databaseFactory = databaseFactory; } /** - * @return the associated {@link ClientSession}. Can be {@literal null}. + * @return the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. */ @Nullable - ClientSession getSession() { - return session; + public CoreTransactionAttemptContext getCore() { + return core; } /** - * @return the required associated {@link ClientSession}. + * @return the required associated {@link CoreTransactionAttemptContext}. * @throws IllegalStateException if no session is associated. */ - ClientSession getRequiredSession() { - - ClientSession session = getSession(); - - if (session == null) { - throw new IllegalStateException("No ClientSession associated"); + CoreTransactionAttemptContext getRequiredCore() { + CoreTransactionAttemptContext core = getCore(); + if (core == null) { + throw new IllegalStateException("No CoreTransactionAttemptContext associated"); } - return session; + return core; } - /** + /* * @return the associated {@link CouchbaseClientFactory}. - */ + ReactiveCouchbaseClientFactory getDatabaseFactory() { return databaseFactory; } + */ /** - * Set the {@link ClientSession} to guard. + * Set the {@link CoreTransactionAttemptContext} to guard. * - * @param session can be {@literal null}. + * @param core can be {@literal null}. */ - void setSession(@Nullable ClientSession session) { - this.session = session; + CoreTransactionAttemptContext setCore(@Nullable CoreTransactionAttemptContext core) { + System.err.println("setCore: " + core); + return this.core = core; } /** * @return {@literal true} if session is not {@literal null}. */ - boolean hasSession() { - return session != null; + boolean hasCore() { + return core != null; } /** - * If the {@link ReactiveCouchbaseResourceHolder} is {@link #hasSession() not already associated} with a - * {@link ClientSession} the given value is {@link #setSession(ClientSession) set} and returned, otherwise the current - * bound session is returned. + * If the {@link ReactiveCouchbaseResourceHolder} is {@link #hasCore() not already associated} with a + * {@link CoreTransactionAttemptContext} the given value is {@link #setCore(CoreTransactionAttemptContext)} set} and + * returned, otherwise the current bound session is returned. * - * @param session + * @param core * @return */ @Nullable - ClientSession setSessionIfAbsent(@Nullable ClientSession session) { + CoreTransactionAttemptContext setSessionIfAbsent(@Nullable CoreTransactionAttemptContext core) { - if (!hasSession()) { - setSession(session); + if (!hasCore()) { + setCore(core); } - return session; + return this.core; } - /** - * @return {@literal true} if the session is active and has not been closed. - */ - boolean hasActiveSession() { - - if (!hasSession()) { - return false; - } - - return hasServerSession() && !getRequiredSession().getServerSession().isClosed(); + public boolean hasActiveTransaction() { + return getCore() != null; } - /** - * @return {@literal true} if the session has an active transaction. - * @see #hasActiveSession() - */ - boolean hasActiveTransaction() { - if (!hasActiveSession()) { - return false; + public TransactionResultHolder transactionResultHolder(Integer key) { + TransactionResultHolder holder = getResultMap.get(key); + if(holder == null){ + throw new RuntimeException("did not find transactionResultHolder for key="+key+" in session"); } - - return getRequiredSession().hasActiveTransaction(); + return holder; } - /** - * @return {@literal true} if the {@link ClientSession} has a {link com.mongodb.session.ServerSession} associated that - * is accessible via {@link ClientSession#getServerSession()}. - */ - boolean hasServerSession() { - - try { - return getRequiredSession().getServerSession() != null; - } catch (IllegalStateException serverSessionClosed) { - // ignore - } - - return false; + public TransactionResultHolder transactionResultHolder(TransactionResultHolder holder, Object o) { + System.err.println("PUT: "+System.identityHashCode(o)+" "+o); + getResultMap.put(System.identityHashCode(o), holder); + return holder; } + } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java index cb8705fd1..90ebf249c 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java @@ -15,13 +15,13 @@ */ package org.springframework.data.couchbase.transaction; -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import com.couchbase.client.java.transactions.config.TransactionOptions; import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.lang.Nullable; -import org.springframework.transaction.ReactiveTransaction; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionSystemException; @@ -33,17 +33,21 @@ import org.springframework.util.ClassUtils; import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.Transactions; /** * A {@link org.springframework.transaction.ReactiveTransactionManager} implementation that manages - * {@link ClientSession} based transactions for a single {@link CouchbaseClientFactory}. + * {@link CoreTransactionAttemptContext} based transactions for a single {@link CouchbaseClientFactory}. *

    - * Binds a {@link ClientSession} from the specified {@link CouchbaseClientFactory} to the subscriber + * Binds a {@link CoreTransactionAttemptContext} from the specified {@link CouchbaseClientFactory} to the subscriber * {@link reactor.util.context.Context}. *

    * {@link org.springframework.transaction.TransactionDefinition#isReadOnly() Readonly} transactions operate on a - * {@link ClientSession} and enable causal consistency, and also {@link ClientSession#startTransaction() start}, - * {@link ClientSession#commitTransaction() commit} or {@link ClientSession#abortTransaction() abort} a transaction. + * {@link CoreTransactionAttemptContext} and enable causal consistency, and also + * {@link CoreTransactionAttemptContext#startTransaction() start}, + * {@link CoreTransactionAttemptContext#commitTransaction() commit} or + * {@link CoreTransactionAttemptContext#abortTransaction() abort} a transaction. *

    * Application code is required to retrieve the {link com.xxxxxxx.reactivestreams.client.MongoDatabase} via {link * org.springframework.data.xxxxxxx.ReactiveMongoDatabaseUtils#getDatabase(CouchbaseClientFactory)} instead of a @@ -65,13 +69,14 @@ public class ReactiveCouchbaseTransactionManager extends AbstractReactiveTransac implements InitializingBean { private @Nullable ReactiveCouchbaseClientFactory databaseFactory; // (why) does this need to be reactive? + private @Nullable Transactions transactions; /** * Create a new {@link ReactiveCouchbaseTransactionManager} for bean-style usage. *

    * Note:The {@link org.springframework.data.couchbase.CouchbaseClientFactory db factory} has to be - * {@link #setDatabaseFactory(ReactiveCouchbaseClientFactory)} set} before using the instance. Use this constructor to prepare - * a {@link ReactiveCouchbaseTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}. + * {@link #setDatabaseFactory(ReactiveCouchbaseClientFactory)} set} before using the instance. Use this constructor to + * prepare a {@link ReactiveCouchbaseTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}. *

    * Optionally it is possible to set default {@link TransactionQueryOptions transaction options} defining {link * com.xxxxxxx.ReadConcern} and {link com.xxxxxxx.WriteConcern}. @@ -93,15 +98,14 @@ public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databa System.err.println("ReactiveCouchbaseTransactionManager : created"); } - /* - public ReactiveCouchbaseTransactionManager(CouchbaseClientFactory databaseFactory, - @Nullable Transactions transactions) { + public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databaseFactory, + @Nullable Transactions transactions) { Assert.notNull(databaseFactory, "DatabaseFactory must not be null!"); - this.databaseFactory = null; // databaseFactory; // should be a clone? TransactionSynchronizationManager binds objs to it + this.databaseFactory = databaseFactory; // databaseFactory; // should be a clone? TransactionSynchronizationManager + // binds objs to it this.transactions = transactions; System.err.println("ReactiveCouchbaseTransactionManager : created Transactions: " + transactions); } -*/ /* * (non-Javadoc) @@ -114,8 +118,8 @@ protected Object doGetTransaction(TransactionSynchronizationManager synchronizat // with an attempt to get the resourceHolder from the synchronizationManager ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager .getResource(getRequiredDatabaseFactory().getCluster().block()); - //TODO ACR from couchbase - //resourceHolder.getSession().setAttemptContextReactive(null); + // TODO ACR from couchbase + // resourceHolder.getSession().setAttemptContextReactive(null); return new ReactiveCouchbaseTransactionObject(resourceHolder); } @@ -142,15 +146,15 @@ protected Mono doBegin(TransactionSynchronizationManager synchronizationMa return Mono.defer(() -> { ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); - + // TODO mr - why aren't we creating the AttemptContext here in the client session in the resourceholder? Mono holder = newResourceHolder(definition, - ClientSessionOptions.builder().causallyConsistent(true).build()); + TransactionOptions.transactionOptions()); return holder.doOnNext(resourceHolder -> { couchbaseTransactionObject.setResourceHolder(resourceHolder); if (logger.isDebugEnabled()) { logger.debug( - String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession()))); + String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); } }).doOnNext(resourceHolder -> { @@ -158,17 +162,18 @@ protected Mono doBegin(TransactionSynchronizationManager synchronizationMa couchbaseTransactionObject.startTransaction(); if (logger.isDebugEnabled()) { - logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession()))); + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getCore()))); } })// .onErrorMap(ex -> new TransactionSystemException( String.format("Could not start Couchbase transaction for session %s.", - debugString(couchbaseTransactionObject.getSession())), + debugString(couchbaseTransactionObject.getCore())), ex)) .doOnSuccess(resourceHolder -> { - System.err.println("ReactiveCouchbaseTransactionManager: "+this); - System.err.println("bindResource: "+getRequiredDatabaseFactory().getCluster().block()+" value: "+resourceHolder); + System.err.println("ReactiveCouchbaseTransactionManager: " + this); + System.err.println( + "bindResource: " + getRequiredDatabaseFactory().getCluster().block() + " value: " + resourceHolder); synchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster().block(), resourceHolder); }).then(); }); @@ -215,12 +220,12 @@ protected final Mono doCommit(TransactionSynchronizationManager synchroniz if (logger.isDebugEnabled()) { logger.debug(String.format("About to doCommit transaction for session %s.", - debugString(couchbaseTransactionObject.getSession()))); + debugString(couchbaseTransactionObject.getCore()))); } return doCommit(synchronizationManager, couchbaseTransactionObject).onErrorMap(ex -> { return new TransactionSystemException(String.format("Could not commit Couchbase transaction for session %s.", - debugString(couchbaseTransactionObject.getSession())), ex); + debugString(couchbaseTransactionObject.getCore())), ex); }); }); } @@ -254,12 +259,12 @@ protected Mono doRollback(TransactionSynchronizationManager synchronizatio if (logger.isDebugEnabled()) { logger.debug(String.format("About to abort transaction for session %s.", - debugString(couchbaseTransactionObject.getSession()))); + debugString(couchbaseTransactionObject.getCore()))); } return couchbaseTransactionObject.abortTransaction().onErrorResume(CouchbaseException.class, ex -> { return Mono.error(new TransactionSystemException(String.format("Could not abort transaction for session %s.", - debugString(couchbaseTransactionObject.getSession())), ex)); + debugString(couchbaseTransactionObject.getCore())), ex)); }); }); } @@ -299,7 +304,7 @@ protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager if (logger.isDebugEnabled()) { logger.debug(String.format("About to release Session %s after transaction.", - debugString(couchbaseTransactionObject.getSession()))); + debugString(couchbaseTransactionObject.getCore()))); } couchbaseTransactionObject.closeSession(); @@ -337,12 +342,12 @@ public void afterPropertiesSet() { } private Mono newResourceHolder(TransactionDefinition definition, - ClientSessionOptions options) { + TransactionOptions options) { ReactiveCouchbaseClientFactory dbFactory = getRequiredDatabaseFactory(); // TODO MSR : config should be derived from config that was used for `transactions` - Mono sess = dbFactory.getSession(options); - return sess.map(session -> new ReactiveCouchbaseResourceHolder(session, dbFactory)); + Mono sess = Mono.just(dbFactory.getTransactionResources(options, null)); + return sess; } /** @@ -372,7 +377,7 @@ private static ReactiveCouchbaseTransactionObject extractCouchbaseTransaction(Ge return (ReactiveCouchbaseTransactionObject) status.getTransaction(); } - private static String debugString(@Nullable ClientSession session) { + private static String debugString(@Nullable CoreTransactionAttemptContext session) { if (session == null) { return "null"; @@ -382,19 +387,7 @@ private static String debugString(@Nullable ClientSession session) { Integer.toHexString(session.hashCode())); try { - if (session.getServerSession() != null) { - debugString += String.format("id = %s, ", session.getServerSession().getIdentifier()); - debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent()); - debugString += String.format("txActive = %s, ", session.hasActiveTransaction()); - debugString += String.format("txNumber = %d, ", session.getServerSession().getTransactionNumber()); - debugString += String.format("closed = %d, ", session.getServerSession().isClosed()); - debugString += String.format("clusterTime = %s", session.getClusterTime()); - } else { - debugString += "id = n/a"; - debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent()); - debugString += String.format("txActive = %s, ", session.hasActiveTransaction()); - debugString += String.format("clusterTime = %s", session.getClusterTime()); - } + debugString += String.format("core=%s", session); } catch (RuntimeException e) { debugString += String.format("error = %s", e.getMessage()); } @@ -438,44 +431,44 @@ final boolean hasResourceHolder() { } /** - * Start a XxxxxxXX transaction optionally given {@link TransactionQueryOptions}. - * todo gp how to expose TransactionOptions + * Start a XxxxxxXX transaction optionally given {@link TransactionQueryOptions}. todo gp how to expose + * TransactionOptions + * * @param options can be {@literal null} */ void startTransaction() { - ClientSession session = getRequiredSession(); - session.startTransaction(); + CoreTransactionAttemptContext core = getRequiredCore(); + // core.startTransaction(); } /** * Commit the transaction. */ public Mono commitTransaction() { - return (Mono)(getRequiredSession().commitTransaction()); + return getRequiredCore().commit(); } /** * Rollback (abort) the transaction. */ public Mono abortTransaction() { - return (Mono)getRequiredSession().abortTransaction(); + return getRequiredCore().rollback(); } /** - * Close a {@link ClientSession} without regard to its transactional state. + * Close a {@link CoreTransactionAttemptContext} without regard to its transactional state. */ void closeSession() { - - ClientSession session = getRequiredSession(); - if (session.getServerSession() != null && !session.getServerSession().isClosed()) { - session.close(); - } + CoreTransactionAttemptContext session = getRequiredCore(); + // if (session.getServerSession() != null && !session.getServerSession().isClosed()) { + // session.close(); + // } } @Nullable - public ClientSession getSession() { - return resourceHolder != null ? resourceHolder.getSession() : null; + public CoreTransactionAttemptContext getCore() { + return resourceHolder != null ? resourceHolder.getCore() : null; } private ReactiveCouchbaseResourceHolder getRequiredResourceHolder() { @@ -484,11 +477,10 @@ private ReactiveCouchbaseResourceHolder getRequiredResourceHolder() { return resourceHolder; } - private ClientSession getRequiredSession() { - - ClientSession session = getSession(); - Assert.state(session != null, "A Session is required but it turned out to be null."); - return session; + private CoreTransactionAttemptContext getRequiredCore() { + CoreTransactionAttemptContext core = getCore(); + Assert.state(core != null, "A CoreTransactionAttemptContext is required but it turned out to be null."); + return core; } /* diff --git a/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java index 69d9a77a8..df90d2b54 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java @@ -20,6 +20,7 @@ import java.util.Optional; import java.util.function.BiFunction; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.core.MethodClassKey; @@ -31,7 +32,7 @@ /** * {@link MethodInterceptor} implementation looking up and invoking an alternative target method having - * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base. + * {@link CoreTransactionAttemptContext} as its first argument. This allows seamless integration with the existing code base. *
    * The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself * like (eg. TODO) and decorate them @@ -47,39 +48,39 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { private static final MethodCache METHOD_CACHE = new MethodCache(); - private final ClientSession session; - private final ClientSessionOperator collectionDecorator; - private final ClientSessionOperator databaseDecorator; + private final ReactiveCouchbaseResourceHolder session; + private final ReactiveCouchbaseResourceHolderOperator collectionDecorator; + private final ReactiveCouchbaseResourceHolderOperator databaseDecorator; private final Object target; private final Class targetType; private final Class collectionType; private final Class databaseType; - private final Class sessionType; + private final Class sessionType; /** * Create a new SessionAwareMethodInterceptor for given target. * - * @param session the {@link ClientSession} to be used on invocation. + * @param session the {@link CoreTransactionAttemptContext} to be used on invocation. * @param target the original target object. * @param databaseType the MongoDB database type - * @param databaseDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive + * @param databaseDecorator a {@link ReactiveCouchbaseResourceHolderOperator} used to create the proxy for an imperative / reactive * {@code MongoDatabase}. * @param collectionType the MongoDB collection type. - * @param collectionDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive + * @param collectionDecorator a {@link ReactiveCouchbaseResourceHolderOperator} used to create the proxy for an imperative / reactive * {@code MongoCollection}. * @param target object type. */ - public SessionAwareMethodInterceptor(ClientSession session, T target, Class sessionType, - Class databaseType, ClientSessionOperator databaseDecorator, Class collectionType, - ClientSessionOperator collectionDecorator) { + public SessionAwareMethodInterceptor(ReactiveCouchbaseResourceHolder session, T target, Class sessionType, + Class databaseType, ReactiveCouchbaseResourceHolderOperator databaseDecorator, Class collectionType, + ReactiveCouchbaseResourceHolderOperator collectionDecorator) { - Assert.notNull(session, "ClientSession must not be null!"); + Assert.notNull(session, "CoreTransactionAttemptContext must not be null!"); Assert.notNull(target, "Target must not be null!"); Assert.notNull(sessionType, "SessionType must not be null!"); Assert.notNull(databaseType, "Database type must not be null!"); - Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null!"); + Assert.notNull(databaseDecorator, "Database CoreTransactionAttemptContextOperator must not be null!"); Assert.notNull(collectionType, "Collection type must not be null!"); - Assert.notNull(collectionDecorator, "Collection ClientSessionOperator must not be null!"); + Assert.notNull(collectionDecorator, "Collection CoreTransactionAttemptContextOperator must not be null!"); this.session = session; this.target = target; @@ -137,14 +138,14 @@ protected Object decorate(Object target) { private static boolean requiresSession(Method method) { if (method.getParameterCount() == 0 - || !ClassUtils.isAssignable(ClientSession.class, method.getParameterTypes()[0])) { + || !ClassUtils.isAssignable(CoreTransactionAttemptContext.class, method.getParameterTypes()[0])) { return true; } return false; } - private static Object[] prependSessionToArguments(ClientSession session, MethodInvocation invocation) { + private static Object[] prependSessionToArguments(ReactiveCouchbaseResourceHolder session, MethodInvocation invocation) { Object[] args = new Object[invocation.getArguments().length + 1]; @@ -155,7 +156,7 @@ private static Object[] prependSessionToArguments(ClientSession session, MethodI } /** - * Simple {@link Method} to {@link Method} caching facility for {@link ClientSession} overloaded targets. + * Simple {@link Method} to {@link Method} caching facility for {@link CoreTransactionAttemptContext} overloaded targets. * * @since 2.1 * @author Christoph Strobl @@ -171,7 +172,7 @@ static class MethodCache { * @param targetClass * @return */ - Optional lookup(Method method, Class targetClass, Class sessionType) { + Optional lookup(Method method, Class targetClass, Class sessionType) { return cache.computeIfAbsent(new MethodClassKey(method, targetClass), val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType))); @@ -179,7 +180,7 @@ Optional lookup(Method method, Class targetClass, Class targetType, - Class sessionType) { + Class sessionType) { Class[] argTypes = sourceMethod.getParameterTypes(); Class[] args = new Class[argTypes.length + 1]; @@ -203,10 +204,10 @@ boolean contains(Method method, Class targetClass) { /** * Represents an operation upon two operands of the same type, producing a result of the same type as the operands - * accepting {@link ClientSession}. This is a specialization of {@link BiFunction} for the case where the operands and + * accepting {@link CoreTransactionAttemptContext}. This is a specialization of {@link BiFunction} for the case where the operands and * the result are all of the same type. * * @param the type of the operands and result of the operator */ - public interface ClientSessionOperator extends BiFunction {} + public interface ReactiveCouchbaseResourceHolderOperator extends BiFunction {} } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java index 2f86a7563..e800f3e25 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java @@ -1,10 +1,10 @@ package org.springframework.data.couchbase.transaction; -import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.TransactionResult; import com.couchbase.client.java.transactions.config.TransactionOptions; -import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.transaction.ReactiveTransaction; import org.springframework.transaction.TransactionDefinition; @@ -14,116 +14,144 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; // todo gp needed now Transactions has gone? public class TransactionsWrapper { - ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; - public TransactionsWrapper(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory){ - this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory; - } + public TransactionsWrapper(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) { + this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory; + } - /** - * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default PerTransactionConfig. - */ - public Mono reactive(Function> transactionLogic) { - // TODO long duration for debugger - Duration duration = Duration.ofMinutes(20); - System.err.println("tx duration of "+duration); - return run(transactionLogic, TransactionOptions.transactionOptions().timeout(duration)); - } + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default + * PerTransactionConfig. + */ + public Mono reactive(Function> transactionLogic) { + // TODO long duration for debugger + Duration duration = Duration.ofMinutes(20); + System.err.println("tx duration of " + duration); + return run(transactionLogic, TransactionOptions.transactionOptions().timeout(duration)); + } - public Mono run(Function> transactionLogic, - TransactionOptions perConfig) { - // todo gp this is duplicating a lot of logic from the core loop, and is hopefully not needed.. - return Mono.empty(); -// TransactionConfig config = TransactionConfigBuilder.create().build(); -// -// ClusterEnvironment env = ClusterEnvironment.builder().build(); -// return Mono.defer(() -> { -// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); -// -// TransactionContext overall = -// new TransactionContext(env.requestTracer(), -// env.eventBus(), -// UUID.randomUUID().toString(), -// now(), -// Duration.ZERO, -// merged); -// AtomicReference startTime = new AtomicReference<>(0L); -// -// Mono ob = Mono.fromCallable(() -> { -// String txnId = UUID.randomUUID().toString(); -// overall.LOGGER.info(configDebug(config, perConfig)); -// return transactions.reactive().createAttemptContext(overall, merged, txnId); -// }).flatMap(ctx -> { -// -// AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", -// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); -// -///* begin spring-data-couchbase transaction 1/2 */ -// ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory -// .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); -// ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, -// reactiveCouchbaseClientFactory); -// Mono sync = TransactionContextManager.currentContext() -// .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { -// synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); -// prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); -///* end spring-data-couchbase transaction 1/2 */ -// Mono result = transactionLogic.apply(ctx); -// result -// .onErrorResume(err -> { -// AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); -// logElidedStacktrace(ctx, err); -// -// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); -// }) -// .thenReturn(ctx); -// return result.then(Mono.just(synchronizationManager)); -// }); -// /* begin spring-data-couchbase transaction 2/2 */ -// return sync.contextWrite(TransactionContextManager.getOrCreateContext()) -// .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(Mono.just(ctx)); -///* end spring-data-couchbase transaction 2/2 */ -// }).doOnSubscribe(v -> startTime.set(System.nanoTime())) -// .doOnNext(v -> AttemptContextReactiveAccessor.getLogger(v).trace(v.attemptId(), "finished attempt %d in %sms", -// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); -// -// return transactions.reactive().executeTransaction(merged, overall, ob) -// .doOnNext(v -> overall.span().finish()) -// .doOnError(err -> overall.span().failWith(err)); -// }); - } + public Mono run(Function> transactionLogic) { + return run(transactionLogic,null); + } + public Mono run(Function> transactionLogic, + TransactionOptions perConfig) { + // todo gp this is duplicating a lot of logic from the core loop, and is hopefully not needed.. + // todo mr it binds to with the TransactionSynchronizationManager - which is necessary. + Mono txResult = reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions().run((ctx) -> { + ReactiveCouchbaseResourceHolder resourceHolder = reactiveCouchbaseClientFactory + .getTransactionResources(TransactionOptions.transactionOptions(), AttemptContextReactiveAccessor.getCore(ctx)); -// private void logElidedStacktrace(ReactiveTransactionAttemptContext ctx, Throwable err) { -// transactions.reactive().logElidedStacktrace(ctx, err); -// } -// -// private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { -// return transactions.reactive().configDebug(config, perConfig); -// } -// - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); - } + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + Mono result = transactionLogic.apply(ctx); + result + .onErrorResume(err -> { + AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.toString(), "caught exception '%s' in async, rethrowing", err); + //logElidedStacktrace(ctx, err); - private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, - ReactiveTransaction status, TransactionDefinition definition) { + return Mono.error(new TransactionOperationFailedException(true, true, err, null)); + }) + .thenReturn(ctx); + return result.then(Mono.just(synchronizationManager)); + }); - // if (status.isNewSynchronization()) { - synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); - synchronizationManager.setCurrentTransactionIsolationLevel( - definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() - : null); - synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); - synchronizationManager.setCurrentTransactionName(definition.getName()); - synchronizationManager.initSynchronization(); - // } - } + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + }); + return txResult; + /* + TransactionsConfig config = TransactionsConfig.create().build(); + + ClusterEnvironment env = ClusterEnvironment.builder().build(); + return Mono.defer(() -> { + MergedTransactionsConfig merged = new MergedTransactionsConfig(config, Optional.of(perConfig)); + + TransactionContext overall = + new TransactionContext(env.requestTracer(), + env.eventBus(), + UUID.randomUUID().toString(), + now(), + Duration.ZERO, + merged); + AtomicReference startTime = new AtomicReference<>(0L); + + Mono ob = Mono.fromCallable(() -> { + String txnId = UUID.randomUUID().toString(); + //overall.LOGGER.info(configDebug(config, perConfig)); + return reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions().createAttemptContext(overall, merged, txnId); + }).flatMap(ctx -> { + + AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", + overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); + + // begin spring-data-couchbase transaction 1/2 * + ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + // end spring-data-couchbase transaction 1/2 + Mono result = transactionLogic.apply(ctx); + result + .onErrorResume(err -> { + AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); + logElidedStacktrace(ctx, err); + + return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); + }) + .thenReturn(ctx); + return result.then(Mono.just(synchronizationManager)); + }); + // begin spring-data-couchbase transaction 2/2 + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(Mono.just(ctx)); + // end spring-data-couchbase transaction 2/2 + }).doOnSubscribe(v -> startTime.set(System.nanoTime())) + .doOnNext(v -> AttemptContextReactiveAccessor.getLogger(v).trace(v.attemptId(), "finished attempt %d in %sms", + overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); + + return transactions.reactive().executeTransaction(merged, overall, ob) + .doOnNext(v -> overall.span().finish()) + .doOnError(err -> overall.span().failWith(err)); + }); + + */ + } + + // private void logElidedStacktrace(ReactiveTransactionAttemptContext ctx, Throwable err) { + // transactions.reactive().logElidedStacktrace(ctx, err); + // } + // + // private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { + // return transactions.reactive().configDebug(config, perConfig); + // } + // + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + + private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, + ReactiveTransaction status, TransactionDefinition definition) { + + // if (status.isNewSynchronization()) { + synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); + synchronizationManager.setCurrentTransactionIsolationLevel( + definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() + : null); + synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); + synchronizationManager.setCurrentTransactionName(definition.getName()); + synchronizationManager.initSynchronization(); + // } + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java b/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.save similarity index 100% rename from src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.java rename to src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.save diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java index c6c9b41d8..a82c532ff 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java @@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.transactions.CouchbaseTemplateTransaction2IntegrationTests; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index a8ba752b1..648ef2a1f 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -28,19 +28,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; -import org.springframework.data.couchbase.transactions.CouchbaseTemplateTransaction2IntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.query.QueryOptions; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * CouchbaseCache tests Theses tests rely on a cb server running. @@ -62,9 +58,9 @@ public void beforeEach() { cache = CouchbaseCacheManager.create(couchbaseTemplate.getCouchbaseClientFactory()).createCouchbaseCache("myCache", CouchbaseCacheConfiguration.defaultCacheConfig()); clear(cache); - //ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - //cacheManager = ac.getBean(CouchbaseCacheManager.class); - //userRepository = ac.getBean(UserRepository.class); + // ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // cacheManager = ac.getBean(CouchbaseCacheManager.class); + // userRepository = ac.getBean(UserRepository.class); } @AfterEach diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index 90b397cd9..a5851e8e6 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -48,7 +48,7 @@ public interface ReactiveAirportRepository @Query("SELECT META(#{#n1ql.bucket}).id AS __id, META(#{#n1ql.bucket}).cas AS __cas, meta().id as id FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} #{[1]}") - @ScanConsistency(query=QueryScanConsistency.NOT_BOUNDED) + @ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS) Flux findIdByDynamicN1ql(String docType, String queryStatement); @Override diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index d06f879ef..f59e85e41 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -141,7 +141,10 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr public void beforeEach() { super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + } @Test diff --git a/src/test/java/org/springframework/data/couchbase/transactions/Config.java b/src/test/java/org/springframework/data/couchbase/transactions/Config.java index 9b916a89a..e9193f294 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/Config.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/Config.java @@ -3,6 +3,8 @@ import java.time.Duration; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.transaction.config.CoreTransactionsConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.springframework.context.annotation.Configuration; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; @@ -39,9 +41,8 @@ public String getBucketName() { } @Override - public TransactionOptions transactionOptions() { - return TransactionOptions.transactionOptions().timeout(Duration.ofMinutes(20)) - .durabilityLevel(DurabilityLevel.MAJORITY); + public TransactionsConfig.Builder transactionsConfig() { + return TransactionsConfig.builder().durabilityLevel(DurabilityLevel.NONE).timeout(Duration.ofMinutes(20));// for testing } /* diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index accbe7d2e..9919adcdb 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -18,19 +18,21 @@ import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; -import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; -import com.couchbase.client.java.transactions.TransactionResult; -import com.couchbase.client.java.transactions.error.TransactionFailedException; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.config.TransactionOptions; import lombok.Data; -import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Duration; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -52,11 +54,12 @@ import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; -import org.springframework.data.couchbase.transaction.ClientSession; import org.springframework.data.couchbase.transaction.ClientSessionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; @@ -78,9 +81,14 @@ import org.springframework.transaction.support.DefaultTransactionDefinition; import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.Transactions; +import com.couchbase.client.java.transactions.error.TransactionFailedException; /** * Tests for com.couchbase.transactions using @@ -104,20 +112,24 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT @Autowired PersonService personService; @Autowired CouchbaseTemplate operations; - //static GenericApplicationContext context; + // if these are changed from default, then beforeEach needs to clean up separately + String sName = "_default"; + String cName = "_default"; + + // static GenericApplicationContext context; @BeforeAll public static void beforeAll() { callSuperBeforeAll(new Object() {}); - //context = new AnnotationConfigApplicationContext(Config.class, PersonService.class); + // context = new AnnotationConfigApplicationContext(Config.class, PersonService.class); } @AfterAll public static void afterAll() { callSuperAfterAll(new Object() {}); - //if (context != null) { - // context.close(); - //} + // if (context != null) { + // context.close(); + // } } @BeforeEach @@ -137,41 +149,6 @@ public void beforeEachTest() { } } - /* Not used in this class. The class itself is not @Transaction - - List>> assertionList; - - @BeforeTransaction - public void beforeTransaction() { - System.err.println("BeforeTransaction"); - assertionList = new ArrayList<>(); - } - - @AfterTransaction - public void afterTransaction() { - System.err.println("AfterTransaction"); - if (assertionList == null) { - return; - } - assertionList.forEach(it -> { - Person p = (Person) (operations.findById(it.getPersistable().getClass()).one(it.getId().toString())); - boolean isPresent = p != null; - System.err.println(("isPresent: " + isPresent + " shouldBePresent: " + it.shouldBePresent())); - assertThat(isPresent).isEqualTo(it.shouldBePresent()) - .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), - it.shouldBePresent() ? "be present" : "NOT be present")); - }); - } - - private AfterTransactionAssertion assertAfterTransaction(Person p) { - AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(p); - if (assertionList != null) { - assertionList.add(assertion); - } - return assertion; - } - */ - @Test public void shouldRollbackAfterException() { Person p = new Person(null, "Walter", "White"); @@ -183,7 +160,7 @@ public void shouldRollbackAfterException() { @Test public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { Person p = new Person(null, "Walter", "White"); - assertThrows(SimulateFailureException.class, () -> personService.declarativeSavePersonErrors(p)); + assertThrowsOneOf(() -> personService.declarativeSavePersonErrors(p), TransactionFailedException.class); Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); assertEquals(0, count, "should have done roll back and left 0 entries"); } @@ -218,7 +195,7 @@ public void replaceInTxAnnotatedCallback() { Person switchedPerson = new Person(1, "Dave", "Reynolds"); cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - Person p = personService.declarativeFindReplacePersonCallback(person, tryCount); + Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); } @@ -279,13 +256,8 @@ public void errorAfterTxShouldNotAffectPreviousStep() { public void replacePersonCBTransactionsRxTmpl() { Person person = new Person(1, "Walter", "White"); cbTmpl.insertById(Person.class).one(person); - Mono result = this.couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get - // the - // ctx - ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, - reactiveCouchbaseClientFactory); + Mono result = this.couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); Mono sync = TransactionContextManager.currentContext() .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); @@ -309,14 +281,8 @@ public void insertPersonCBTransactionsRxTmplRollback() { try { rxCBTmpl.removeById(Person.class).one(person.getId().toString()); } catch (DocumentNotFoundException dnfe) {} - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get - // the - // ctx - - ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, - reactiveCouchbaseClientFactory); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); Mono sync = TransactionContextManager.currentContext() .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); @@ -339,14 +305,11 @@ public void insertPersonCBTransactionsRxTmplRollback() { @Test public void insertTwicePersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); - sleepMs(1000); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get - // the - // ctx - ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, - reactiveCouchbaseClientFactory); + // + Transactions transactions = this.couchbaseClientFactory.getCluster().transactions(); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { + + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); Mono sync = TransactionContextManager.currentContext() .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); @@ -366,6 +329,11 @@ public void insertTwicePersonCBTransactionsRxTmplRollback() { assertNull(pFound, "insert should have been rolled back"); } + /** + * This test has the bare minimum for reactive transactions. Create the ClientSession that holds the ctx and put it in + * a resourceHolder and binds it to the currentContext. The retries are handled by couchbase-transactions - which + * creates a new ctx and re-runs the lambda. This is essentially what TransactionWrapper does. + */ @Test public void replaceWithCasConflictResolvedViaRetry() { Person person = new Person(1, "Walter", "White"); @@ -376,14 +344,8 @@ public void replaceWithCasConflictResolvedViaRetry() { cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get - // the - // ctx - // see TransactionalOperatorImpl.transactional(). - ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, - reactiveCouchbaseClientFactory); + Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); Mono sync = TransactionContextManager.currentContext() .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); @@ -397,7 +359,6 @@ public void replaceWithCasConflictResolvedViaRetry() { .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)) // .then(Mono.just(synchronizationManager)); // tx }); - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); }); @@ -415,12 +376,12 @@ public void wrapperReplaceWithCasConflictResolvedViaRetry() { Person person = new Person(1, "Walter", "White"); Person switchedPerson = new Person(1, "Dave", "Reynolds"); AtomicInteger tryCount = new AtomicInteger(0); + cbTmpl.insertById(Person.class).one(person); for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try AtomicBoolean stop = new AtomicBoolean(false); Thread t = new ReplaceLoopThread(stop, switchedPerson); t.start(); - cbTmpl.insertById(Person.class).one(person); tryCount.set(0); TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); Mono result = transactionsWrapper.reactive(ctx -> { @@ -451,38 +412,57 @@ public void wrapperReplaceWithCasConflictResolvedViaRetry() { public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { Person person = new Person(1, "Walter", "White"); Person switchedPerson = new Person(1, "Dave", "Reynolds"); - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); - t.start(); - cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - Person p = personService.declarativeFindReplacePersonCallback(person, tryCount); - stop.set(true); - System.out.println("person: " + p); + cbTmpl.insertById(Person.class).one(person); + + for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + tryCount.set(0); + + Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount); + stop.set(true); + if (tryCount.get() > 1) { + break; + } + } Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); - assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + } /** - * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. I don't know why - * it isn't retried. This seems like it is due to the functioning of AbstractReactiveTransactionManager + * Reactive @Transactional does not retry write-write conflicts. It throws RetryTransactionException up to the client + * and expects the client to retry. */ @Test public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { Person person = new Person(1, "Walter", "White"); Person switchedPerson = new Person(1, "Dave", "Reynolds"); - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); - t.start(); cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - Person p = personService.declarativeFindReplacePersonReactive(person, tryCount).block(); - stop.set(true); - System.out.println("person: " + p); + for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + tryCount.set(0); + for (;;) { + Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount) + .onErrorResume(RetryTransactionException.class, (thrown) -> Mono.empty()).block(); + if (res != null) + break; + } + stop.set(true); + if (tryCount.get() > 1) { + break; + } + + } Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); - assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); } @Test @@ -493,28 +473,35 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { public void replaceWithCasConflictResolvedViaRetryAnnotated() { Person person = new Person(1, "Walter", "White"); Person switchedPerson = new Person(1, "Dave", "Reynolds"); - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson, 100); - t.start(); cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - Person p = personService.declarativeFindReplacePerson(person, tryCount); - stop.set(true); - System.out.println("person: " + p); + + for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try + AtomicBoolean stop = new AtomicBoolean(false); + Thread t = new ReplaceLoopThread(stop, switchedPerson); + t.start(); + tryCount.set(0); + Person p = personService.declarativeFindReplacePerson(person, tryCount); + stop.set(true); + if (tryCount.get() > 1) { + break; + } + } Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); - assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + System.out.println("pFound: " + pFound); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); } private class ReplaceLoopThread extends Thread { AtomicBoolean stop; Person person; - int maxIterations=100; + int maxIterations = 100; public ReplaceLoopThread(AtomicBoolean stop, Person person, int... iterations) { this.stop = stop; this.person = person; - if(iterations != null && iterations.length ==1 ){ + if (iterations != null && iterations.length == 1) { this.maxIterations = iterations[0]; } } @@ -535,223 +522,190 @@ public void run() { } } - /* - @Test + + @Test public void replacePersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); String newName = "Walt"; rxCBTmpl.insertById(Person.class).one(person).block(); - sleepMs(1000); - Mono result = transactions.reactive(((ctx) -> { // get the ctx - // can we take the ReactiveTransactionAttemptContext ctx and save it in the context? - ClientSession clientSession = couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), ctx); - CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(clientSession, couchbaseClientFactory); - - // I think this needs to happen within the couchbaseClientFactory.getCluster().reactive().transactions().run() call - or equivalent. - - // this currentContext() call is going to create a new ctx, and store the acr. Will it get uses in syncFlatMap() - // below? Should the ctx be created in the above call to couchbaseClientFactory.getCluster().reactive().transactions().run()? - // How does this work in savePerson etc? - // is there means for just getting the currentContext() without creating it? - Mono sync = TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new) - .flatMap(synchronizationManager -> { - synchronizationManager.bindResource(couchbaseClientFactory, resourceHolder); // is this binding to the right syncManager? - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - return Mono.just(synchronizationManager); - }).contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()); - - - return sync.flatMap( (ignore) -> { - System.out.println("TSM: "+ignore); - return rxCBTmpl.findById(Person.class) - .one(person.getId().toString()); }) // need to get the TSM context in the one() calls. - .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName("Walt"))).then(Mono.empty())); - })); - - + + Mono result = reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions() + .run(ctx -> { + // can we take the ReactiveTransactionAttemptContext ctx and save it in the context? + // how we get from non-reactive to reactive? + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); + + assertEquals(reactiveCouchbaseClientFactory.getCluster().block(), couchbaseClientFactory.getCluster()); + // I think this needs to happen within the couchbaseClientFactory.getCluster().reactive().transactions().run() + // call - or equivalent. + // this currentContext() call is going to create a new ctx, and store the acr. Will it get used in + // syncFlatMap() + // below? Should the ctx be created in the above call to + // couchbaseClientFactory.getCluster().reactive().transactions().run()? + // How does this work in savePerson etc? + // is there means for just getting the currentContext() without creating it? + return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new) + .flatMap(synchronizationManager -> { + // is this the correct sync Manager?? + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), + resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + return rxCBTmpl.findById(Person.class).one(person.getId().toString()); + }) // need to get the TSM context in the one() calls. + .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName(newName))).then(Mono.empty()); + }).contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + result.block(); - // assertThrows(TransactionFailedException.class, () -> result.block()); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); System.err.println(pFound); - assertEquals(person.getFirstname(), pFound.getFirstname()); - } - - */ + assertEquals(newName, pFound.getFirstname()); + } - /* @Test public void deletePersonCBTransactionsRxTmpl() { Person person = new Person(1, "Walter", "White"); - rxCBTmpl.insertById(Person.class).one(person).block(); - sleepMs(1000); - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxCBTmpl.removeById(Person.class).transaction(new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager)).one(person.getId().toString()) - .then(); - })); + remove(cbTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person).then(); + }); result.block(); - Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxTmplFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person) + .then(rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person)).then(); + }); + assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// + + @Test + public void deletePersonCBTransactionsRxRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> { // get the ctx + return rxRepo.withCollection(cName).delete(person).then(); + }); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); assertNull(pFound, "Should not have found " + pFound); } - - @Test - public void deletePersonCBTransactionsRxTmpl() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) - .then(); - })); - result.block(); - Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); - assertNull(pFound, "Should not have found " + pFound); - } - - @Test - public void deletePersonCBTransactionsRxTmplFail() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) - .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) - .then(); - })); - assertThrows(TransactionFailedException.class, result::block); - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(pFound, person, "Should have found " + person); - } - - // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// - - @Test - public void deletePersonCBTransactionsRxRepo() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - rxRepo.withCollection(cName).save(person).block(); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); - })); - result.block(); - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should not have found " + pFound); - } - - @Test - public void deletePersonCBTransactionsRxRepoFail() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - rxRepo.withCollection(cName).save(person).block(); - - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) - .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); - })); - assertThrows(TransactionFailedException.class, result::block); - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(pFound, person, "Should have found " + person); - } - - @Test - public void findPersonCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - List docs = new LinkedList<>(); - Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - Mono result = transactions.reactive(((ctx) -> { // get the ctx - return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { - docs.add(doc); - return doc; - }).then(); - })); - result.block(); - assertFalse(docs.isEmpty(), "Should have found " + person); - for (Object o : docs) { - assertEquals(o, person, "Should have found " + person); - } - } - - @Test - // @Transactional - // Failed to retrieve PlatformTransactionManager for @Transactional test: - public void insertPersonRbCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - - Mono result = transactions.reactive((ctx) -> { // get the ctx - return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) - . flatMap(it -> Mono.error(new PoofException())).then(); - }); - - try { - result.block(); - } catch (TransactionFailedException e) { - e.printStackTrace(); - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should not have found " + pFound); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator - // Failed to retrieve PlatformTransactionManager for @Transactional test: - public void replacePersonRbCBTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - Mono result = transactions.reactive((ctx) -> { // get the ctx - return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) - .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) - .one(pFound.withFirstName("Walt"))) - . flatMap(it -> Mono.error(new PoofException())).then(); - }); - - try { - result.block(); - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void findPersonSpringTransactions() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - List docs = new LinkedList<>(); - Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - Mono result = transactions.reactive((ctx) -> { // get the ctx - return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { - docs.add(doc); - return doc; - }).then(); - }); - result.block(); - assertFalse(docs.isEmpty(), "Should have found " + person); - for (Object o : docs) { - assertEquals(o, person, "Should have found " + person); - } - } - */ + + @Test + public void deletePersonCBTransactionsRxRepoFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> { // get the ctx + return rxRepo.withCollection(cName).delete(person).then(rxRepo.withCollection(cName).delete(person)).then(); + }); + assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void findPersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class).inScope(sName) + .inCollection(cName).matching(q).withConsistency(REQUEST_PLUS).one().doOnSuccess(doc -> docs.add(doc))); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + @Test + public void insertPersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.insertById(Person.class).inScope(sName) + .inCollection(cName).one(person). flatMap(it -> Mono.error(new SimulateFailureException()))); + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void replacePersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.findById(Person.class).inScope(sName) + .inCollection(cName).one(person.getId().toString()).flatMap(pFound -> rxCBTmpl.replaceById(Person.class) + .inScope(sName).inCollection(cName).one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new SimulateFailureException()))); + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = cbTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void findPersonSpringTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); + // sleepMs(1000); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); + Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class).inScope(sName) + .inCollection(cName).matching(q).one().doOnSuccess(r -> docs.add(r))); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + void remove(Collection col, String id) { remove(col.reactive(), id); } @@ -921,17 +875,14 @@ public Person savePerson(Person person) { } public Long countDuringTx(Person person) { - assertInAnnotationTransaction(false); assertInAnnotationTransaction(false); TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, new DefaultTransactionDefinition()); - return personOperationsRx.insertById(Person.class).one(person)// .then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) .as(transactionalOperator::transactional).block(); } - // @Transactional public List saveWithLogs(Person person) { assertInAnnotationTransaction(false); TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, @@ -968,7 +919,7 @@ public List saveWithErrorLogs(Person person) { // org.springframework.beans.factory.NoUniqueBeanDefinitionException: // No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single // matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) public Person declarativeSavePerson(Person person) { assertInAnnotationTransaction(true); return personOperations.insertById(Person.class).one(person); @@ -982,7 +933,7 @@ public Person savePersonBlocking(Person person) { } - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) public Person declarativeSavePersonErrors(Person person) { assertInAnnotationTransaction(true); Person p = personOperations.insertById(Person.class).one(person); // @@ -998,7 +949,7 @@ public Person declarativeSavePersonErrors(Person person) { * @param person * @return */ - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); @@ -1008,11 +959,12 @@ public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger // + org.springframework.transaction.support.TransactionSynchronizationManager // .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); Person p = personOperations.findById(Person.class).one(person.getId().toString()); - return personOperations.replaceById(Person.class).one(p); + return personOperations.replaceById(Person.class).one(p.withFirstName(person.getFirstname())); } /** - * to execute while ThreadReplaceloop() is running should force a retry + * The ReactiveCouchbaseTransactionManager does not retry on write-write conflict. Instead it will throw + * RetryTransactionException to execute while ThreadReplaceloop() is running should force a retry * * @param person * @return @@ -1028,7 +980,7 @@ public Mono declarativeFindReplacePersonReactive(Person person, AtomicIn }).block(); */ return personOperationsRx.findById(Person.class).one(person.getId().toString()) - .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p)); + .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(person.getFirstname()))); } /** @@ -1037,7 +989,7 @@ public Mono declarativeFindReplacePersonReactive(Person person, AtomicIn * @param person * @return */ - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) // doesn't retry + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet()); @@ -1059,22 +1011,6 @@ public Mono declarativeSavePersonErrorsReactive(Person person) { return p; } - void assertInAnnotationTransaction(boolean inTransaction) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - for (StackTraceElement ste : stack) { - if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") || - ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { - if (inTransaction) { - return; - } - } - } - if (!inTransaction) { - return; - } - throw new RuntimeException( - "in-transaction = " + (!inTransaction) + " but expected in-annotation-transaction = " + inTransaction); - } } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java index 2c703f9b4..cf6c934c8 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -1,609 +1,612 @@ -///* -// * Copyright 2012-2021 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.Cluster; -//import lombok.Data; -//import org.springframework.context.annotation.AnnotationConfigApplicationContext; -//import org.springframework.context.annotation.Bean; -//import org.springframework.data.couchbase.config.BeanNames; -//import org.springframework.data.couchbase.core.CouchbaseOperations; -//import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; -//import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; -//import org.springframework.data.domain.Persistable; -//import org.springframework.test.context.transaction.AfterTransaction; -//import org.springframework.test.context.transaction.BeforeTransaction; -//import reactor.core.publisher.Flux; -//import reactor.core.publisher.Mono; -//import reactor.test.StepVerifier; -// -//import java.time.Duration; -//import java.util.ArrayList; -//import java.util.List; -//import java.util.UUID; -// -//import org.junit.jupiter.api.AfterAll; -//import org.junit.jupiter.api.BeforeAll; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.context.support.GenericApplicationContext; -//import org.springframework.dao.DataRetrievalFailureException; -//import org.springframework.data.couchbase.CouchbaseClientFactory; -//import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -//import org.springframework.data.couchbase.core.CouchbaseTemplate; -//import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -//import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -//import org.springframework.data.couchbase.core.query.Query; -//import org.springframework.data.couchbase.domain.Person; -//import org.springframework.data.couchbase.domain.PersonRepository; -//import org.springframework.data.couchbase.domain.ReactivePersonRepository; -//import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; -//import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; -//import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; -//import org.springframework.data.couchbase.util.Capabilities; -//import org.springframework.data.couchbase.util.ClusterType; -//import org.springframework.data.couchbase.util.IgnoreWhen; -//import org.springframework.data.couchbase.util.JavaIntegrationTests; -//import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -//import org.springframework.transaction.annotation.Transactional; -//import org.springframework.transaction.reactive.TransactionalOperator; -//import org.springframework.transaction.support.DefaultTransactionDefinition; -// -//import com.couchbase.client.core.cnc.Event; -//import com.couchbase.client.core.error.DocumentNotFoundException; -//import com.couchbase.client.java.Collection; -//import com.couchbase.client.java.ReactiveCollection; -//import com.couchbase.client.java.kv.RemoveOptions; -// -//import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; -//import static org.assertj.core.api.Assertions.assertThat; -// -///** -// * Tests for com.couchbase.transactions without using the spring data transactions framework -// * -// * @author Michael Reiche -// */ -//@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -//@SpringJUnitConfig(CouchbasePersonTransactionReactiveIntegrationTests.Config.class) -////@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) -//public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { -// -// @Autowired CouchbaseClientFactory couchbaseClientFactory; -// @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; -// @Autowired CouchbaseSimpleCallbackTransactionManager couchbaseTransactionManager; -// @Autowired ReactivePersonRepository rxRepo; -// @Autowired PersonRepository repo; -// @Autowired ReactiveCouchbaseTemplate rxCBTmpl; -// -// @Autowired Cluster myCluster; -// -// /* DO NOT @Autowired */ PersonService personService; -// -// static GenericApplicationContext context; -// @Autowired ReactiveCouchbaseTemplate operations; -// -// @BeforeAll -// public static void beforeAll() { -// callSuperBeforeAll(new Object() {}); -// context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionReactiveIntegrationTests.Config.class, -// CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); -// } -// -// @AfterAll -// public static void afterAll() { -// callSuperAfterAll(new Object() {}); -// } -// -// @BeforeEach -// public void beforeEachTest() { -// personService = context.getBean(CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); // getting it via autowired results in no @Transactional -// operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); -// operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); -// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); -// operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); -// } -// -// -// @Test // DATAMONGO-2265 -// public void shouldRollbackAfterException() { -// personService.savePersonErrors(new Person(null, "Walter", "White")) // -// .as(StepVerifier::create) // -// .verifyError(RuntimeException.class); -// // operations.findByQuery(Person.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); -// // sleepMs(5000); -// operations.count(new Query(), Person.class) // -// .as(StepVerifier::create) // -// .expectNext(0L) // -// .verifyComplete(); -// } -// -// @Test // DATAMONGO-2265 -// // @Rollback(false) -// public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { -// Person p = new Person(null, "Walter", "White"); -// try { -// personService.declarativeSavePersonErrors(p) // -// .as(StepVerifier::create) // -// .expectComplete(); -// // .verifyError(RuntimeException.class); -// } catch (RuntimeException e) { -// if (e instanceof SimulateFailureException || (e.getMessage() != null && e.getMessage().contains("poof"))) { -// System.err.println(e); -// } else { -// throw e; -// } -// } -// -// } -// -// @Test // DATAMONGO-2265 -// public void commitShouldPersistTxEntries() { -// -// personService.savePerson(new Person(null, "Walter", "White")) // -// .as(StepVerifier::create) // -// .expectNextCount(1) // -// .verifyComplete(); -// -// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count().block(); -// -// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // -// .as(StepVerifier::create) // -// .expectNext(1L) // -// .verifyComplete(); -// } -// -// @Test // DATAMONGO-2265 -// public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { -// -// personService.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // -// .expectNextCount(1) // -// .verifyComplete(); -// -// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // -// .as(StepVerifier::create) // -// .expectNext(1L) // -// .verifyComplete(); -// -// } -// -// @Test // DATAMONGO-2265 -// public void commitShouldPersistTxEntriesAcrossCollections() { -// -// personService.saveWithLogs(new Person(null, "Walter", "White")) // -// .then() // -// .as(StepVerifier::create) // -// .verifyComplete(); -// -// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // -// .as(StepVerifier::create) // -// .expectNext(1L) // -// .verifyComplete(); -// -// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // -// .as(StepVerifier::create) // -// .expectNext(4L) // -// .verifyComplete(); -// } -// -// @Test // DATAMONGO-2265 -// public void rollbackShouldAbortAcrossCollections() { -// -// personService.saveWithErrorLogs(new Person(null, "Walter", "White")) // -// .then() // -// .as(StepVerifier::create) // -// .verifyError(); -// -// operations.count(new Query(), Person.class) // -// .as(StepVerifier::create) // -// .expectNext(0L) // -// .verifyComplete(); -// -// operations.count(new Query(), EventLog.class)// -// .as(StepVerifier::create) // -// .expectNext(0L) // -// .verifyComplete(); -// } -// -// @Test // DATAMONGO-2265 -// public void countShouldWorkInsideTransaction() { -// -// personService.countDuringTx(new Person(null, "Walter", "White")) // -// .as(StepVerifier::create) // -// .expectNext(1L) // -// .verifyComplete(); -// } -// -// @Test // DATAMONGO-2265 -// public void emitMultipleElementsDuringTransaction() { -// -// try { -// personService.saveWithLogs(new Person(null, "Walter", "White")) // -// .as(StepVerifier::create) // -// .expectNextCount(4L) // -// .verifyComplete(); -// } catch (Exception e) { -// System.err.println("Done"); -// throw e; -// } -// } -// -// @Test // DATAMONGO-2265 -// public void errorAfterTxShouldNotAffectPreviousStep() { -// -// Person p = new Person(1, "Walter", "White"); -// remove(couchbaseTemplate, "_default", p.getId().toString()); -// personService.savePerson(p) // -// //.delayElement(Duration.ofMillis(100)) // -// .then(Mono.error(new RuntimeException("my big bad evil error"))).as(StepVerifier::create) // -// .expectError() -// .verify(); -// //.expectError() // -// //.as(StepVerifier::create) -// //.expectNext(p) -// //.verifyComplete(); -// -// operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // -// .as(StepVerifier::create) // -// .expectNext(1L) // -// .verifyComplete(); -// } -// -// // @RequiredArgsConstructor -// static class PersonService { -// -// final ReactiveCouchbaseOperations personOperationsRx; -// final ReactiveCouchbaseTransactionManager managerRx; -// final CouchbaseOperations personOperations; -// final CouchbaseCallbackTransactionManager manager; -// -// public PersonService(CouchbaseOperations ops, CouchbaseCallbackTransactionManager mgr, ReactiveCouchbaseOperations opsRx, -// ReactiveCouchbaseTransactionManager mgrRx) { -// personOperations = ops; -// manager = mgr; -// System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); -//// System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); -// System.err.println("manager Manager : " + manager); -// -// personOperationsRx = opsRx; -// managerRx = mgrRx; -// System.out -// .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); -// System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); -// System.out.println("managerRx Manager : " + managerRx); -// return; -// } -// -// public Mono savePersonErrors(Person person) { -// -// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx); -// return personOperationsRx.insertById(Person.class).one(person) // -// . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // -// .as(transactionalOperator::transactional); -// } -// -// public Mono savePerson(Person person) { -// -// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, -// new DefaultTransactionDefinition()); -// -// return personOperationsRx.insertById(Person.class).one(person) // -// .flatMap(Mono::just) // -// .as(transactionalOperator::transactional); -// } -// -// public Mono countDuringTx(Person person) { -// -// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, -// new DefaultTransactionDefinition()); -// -// return personOperationsRx.save(person) // -// .then(personOperationsRx.count(new Query(), Person.class)) // -// .as(transactionalOperator::transactional); -// } -// -// public Flux saveWithLogs(Person person) { -// -// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, -// new DefaultTransactionDefinition()); -// -// return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId().toString(), "beforeConvert")), // -// personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // -// personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // -// personOperationsRx.save(person), // -// personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // -// .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // -// .as(transactionalOperator::transactional); -// } -// -// public Flux saveWithErrorLogs(Person person) { -// -// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, -// new DefaultTransactionDefinition()); -// -// return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId(), "beforeConvert")), // -// personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // -// personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // -// personOperationsRx.save(person), // -// personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // -// . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // -// .as(transactionalOperator::transactional); -// } -// -// @Transactional -// public Flux declarativeSavePerson(Person person) { -// -// TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, -// new DefaultTransactionDefinition()); -// -// return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); -// } -// -// @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) -// public Flux declarativeSavePersonErrors(Person person) { -// Person p = personOperations.insertById(Person.class).one(person); -// // if(1==1)throw new RuntimeException("poof!"); -// Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0); -// System.err.println("pp=" + pp); -// SimulateFailureException.throwEx(); -// return Flux.just(p); -// } -// } -// -// /* -// @Test -// public void deletePersonCBTransactionsRxTmpl() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); -// -// Mono result = transactions.reactive(((ctx) -> { // get the ctx -// return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) -// .then(); -// })); -// result.block(); -// Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); -// assertNull(pFound, "Should not have found " + pFound); -// } -// -// @Test -// public void deletePersonCBTransactionsRxTmplFail() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// cbTmpl.insertById(Person.class).inCollection(cName).one(person); -// -// Mono result = transactions.reactive(((ctx) -> { // get the ctx -// return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) -// .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) -// .then(); -// })); -// assertThrows(TransactionFailedException.class, result::block); -// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); -// assertEquals(pFound, person, "Should have found " + person); -// } -// -// // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// -// -// @Test -// public void deletePersonCBTransactionsRxRepo() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// rxRepo.withCollection(cName).save(person).block(); -// -// Mono result = transactions.reactive(((ctx) -> { // get the ctx -// return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); -// })); -// result.block(); -// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); -// assertNull(pFound, "Should not have found " + pFound); -// } -// -// @Test -// public void deletePersonCBTransactionsRxRepoFail() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// rxRepo.withCollection(cName).save(person).block(); -// -// Mono result = transactions.reactive(((ctx) -> { // get the ctx -// return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) -// .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); -// })); -// assertThrows(TransactionFailedException.class, result::block); -// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); -// assertEquals(pFound, person, "Should have found " + person); -// } -// -// @Test -// public void findPersonCBTransactions() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// cbTmpl.insertById(Person.class).inCollection(cName).one(person); -// List docs = new LinkedList<>(); -// Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); -// Mono result = transactions.reactive(((ctx) -> { // get the ctx -// return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { -// docs.add(doc); -// return doc; -// }).then(); -// })); -// result.block(); -// assertFalse(docs.isEmpty(), "Should have found " + person); -// for (Object o : docs) { -// assertEquals(o, person, "Should have found " + person); -// } -// } -// -// @Test -// // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator -// // Failed to retrieve PlatformTransactionManager for @Transactional test: -// public void insertPersonRbCBTransactions() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// -// Mono result = transactions.reactive((ctx) -> { // get the ctx -// return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) -// . flatMap(it -> Mono.error(new PoofException())).then(); -// }); -// -// try { -// result.block(); -// } catch (TransactionFailedException e) { -// e.printStackTrace(); -// if (e.getCause() instanceof PoofException) { -// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); -// assertNull(pFound, "Should not have found " + pFound); -// return; -// } else { -// e.printStackTrace(); -// } -// } -// throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); -// } -// -// @Test -// // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator -// // Failed to retrieve PlatformTransactionManager for @Transactional test: -// public void replacePersonRbCBTransactions() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// cbTmpl.insertById(Person.class).inCollection(cName).one(person); -// Mono result = transactions.reactive((ctx) -> { // get the ctx -// return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) -// .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) -// .one(pFound.withFirstName("Walt"))) -// . flatMap(it -> Mono.error(new PoofException())).then(); -// }); -// -// try { -// result.block(); -// } catch (TransactionFailedException e) { -// if (e.getCause() instanceof PoofException) { -// Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); -// assertEquals(person, pFound, "Should have found " + person); -// return; -// } else { -// e.printStackTrace(); -// } -// } -// throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); -// } -// -// @Test -// public void findPersonSpringTransactions() { -// Person person = new Person(1, "Walter", "White"); -// remove(cbTmpl, cName, person.getId().toString()); -// cbTmpl.insertById(Person.class).inCollection(cName).one(person); -// List docs = new LinkedList<>(); -// Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); -// Mono result = transactions.reactive((ctx) -> { // get the ctx -// return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { -// docs.add(doc); -// return doc; -// }).then(); -// }); -// result.block(); -// assertFalse(docs.isEmpty(), "Should have found " + person); -// for (Object o : docs) { -// assertEquals(o, person, "Should have found " + person); -// } -// } -// */ -// void remove(Collection col, String id) { -// remove(col.reactive(), id); -// } -// -// void remove(ReactiveCollection col, String id) { -// try { -// col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); -// } catch (DocumentNotFoundException nfe) { -// System.out.println(id + " : " + "DocumentNotFound when deleting"); -// } -// } -// -// void remove(CouchbaseTemplate template, String collection, String id) { -// remove(template.reactive(), collection, id); -// } -// -// void remove(ReactiveCouchbaseTemplate template, String collection, String id) { -// try { -// template.removeById(Person.class).inCollection(collection).one(id).block(); -// System.out.println("removed " + id); -// } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { -// System.out.println(id + " : " + "DocumentNotFound when deleting"); -// } -// } -// -// @Configuration -// @EnableCouchbaseRepositories("org.springframework.data.couchbase") -// @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") -// static class Config extends AbstractCouchbaseConfiguration { -// -// @Override -// public String getConnectionString() { -// return connectionString(); -// } -// -// @Override -// public String getUserName() { -// return config().adminUsername(); -// } -// -// @Override -// public String getPassword() { -// return config().adminPassword(); -// } -// -// @Override -// public String getBucketName() { -// return bucketName(); -// } -// -// @Bean -// public Cluster couchbaseCluster() { -// return Cluster.connect("10.144.220.101", "Administrator", "password"); -// } -// -// /* -// @Bean("personService") -// PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, -// ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { -// return new PersonService(ops, mgr, opsRx, mgrRx); -// } -// */ -// -// } -// -// @Data -// // @AllArgsConstructor -// static class EventLog { -// public EventLog() {} -// -// public EventLog(ObjectId oid, String action) { -// this.id = oid.toString(); -// this.action = action; -// } -// -// public EventLog(String id, String action) { -// this.id = id; -// this.action = action; -// } -// -// String id; -// String action; -// } -//} +/* + * Copyright 2012-2021 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.Cluster; +import lombok.Data; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.annotation.Version; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.domain.Persistable; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbasePersonTransactionReactiveIntegrationTests.Config.class) +//@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + @Autowired CouchbaseSimpleCallbackTransactionManager couchbaseTransactionManager; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + + @Autowired Cluster myCluster; + + /* DO NOT @Autowired */ PersonService personService; + + static GenericApplicationContext context; + @Autowired ReactiveCouchbaseTemplate operations; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionReactiveIntegrationTests.Config.class, + CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + personService = context.getBean(CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); // getting it via autowired results in no @Transactional + operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + } + + + @Test // DATAMONGO-2265 + public void shouldRollbackAfterException() { + personService.savePersonErrors(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .verifyError(RuntimeException.class); + // operations.findByQuery(Person.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); + // sleepMs(5000); + operations.count(new Query(), Person.class) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + // @Rollback(false) + public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { + Person p = new Person(null, "Walter", "White"); + try { + personService.declarativeSavePersonErrors(p) // + .as(StepVerifier::create) // + .expectComplete(); + // .verifyError(RuntimeException.class); + } catch (RuntimeException e) { + if (e instanceof SimulateFailureException || (e.getMessage() != null && e.getMessage().contains("poof"))) { + System.err.println(e); + } else { + throw e; + } + } + + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntries() { + + personService.savePerson(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count().block(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { + + personService.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntriesAcrossCollections() { + + personService.saveWithLogs(new Person(null, "Walter", "White")) // + .then() // + .as(StepVerifier::create) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(4L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void rollbackShouldAbortAcrossCollections() { + + personService.saveWithErrorLogs(new Person(null, "Walter", "White")) // + .then() // + .as(StepVerifier::create) // + .verifyError(); + + operations.count(new Query(), Person.class) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + + operations.count(new Query(), EventLog.class)// + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void countShouldWorkInsideTransaction() { + + personService.countDuringTx(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void emitMultipleElementsDuringTransaction() { + + try { + personService.saveWithLogs(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNextCount(4L) // + .verifyComplete(); + } catch (Exception e) { + System.err.println("Done"); + throw e; + } + } + + @Test // DATAMONGO-2265 + public void errorAfterTxShouldNotAffectPreviousStep() { + + Person p = new Person(1, "Walter", "White"); + //remove(couchbaseTemplate, "_default", p.getId().toString()); + personService.savePerson(p) // + //.delayElement(Duration.ofMillis(100)) // + .then(Mono.error(new RuntimeException("my big bad evil error"))).as(StepVerifier::create) // + .expectError() + .verify(); + //.expectError() // + //.as(StepVerifier::create) + //.expectNext(p) + //.verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + // @RequiredArgsConstructor + static class PersonService { + + final ReactiveCouchbaseOperations personOperationsRx; + final ReactiveCouchbaseTransactionManager managerRx; + final CouchbaseOperations personOperations; + //final CouchbaseCallbackTransactionManager manager; + + public PersonService(CouchbaseOperations ops, /* CouchbaseCallbackTransactionManager mgr, */ ReactiveCouchbaseOperations opsRx, + ReactiveCouchbaseTransactionManager mgrRx) { + personOperations = ops; + //manager = mgr; + System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); +// System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); + //System.err.println("manager Manager : " + manager); + + personOperationsRx = opsRx; + managerRx = mgrRx; + System.out + .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); + System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); + System.out.println("managerRx Manager : " + managerRx); + return; + } + + public Mono savePersonErrors(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx); + return personOperationsRx.insertById(Person.class).one(person) // + . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // + .as(transactionalOperator::transactional); + } + + public Mono savePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class).one(person) // + .flatMap(Mono::just) // + .as(transactionalOperator::transactional); + } + + public Mono countDuringTx(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.save(person) // + .then(personOperationsRx.count(new Query(), Person.class)) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithLogs(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId().toString(), "beforeConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.save(person), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithErrorLogs(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId(), "beforeConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.save(person), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // + . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // + .as(transactionalOperator::transactional); + } + + @Transactional + public Flux declarativeSavePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Flux declarativeSavePersonErrors(Person person) { + Person p = personOperations.insertById(Person.class).one(person); + // if(1==1)throw new RuntimeException("poof!"); + Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0); + System.err.println("pp=" + pp); + SimulateFailureException.throwEx(); + return Flux.just(p); + } + } + + /* + @Test + public void deletePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(); + })); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxTmplFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) + .then(); + })); + assertThrows(TransactionFailedException.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// + + @Test + public void deletePersonCBTransactionsRxRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); + })); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxRepoFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) + .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); + })); + assertThrows(TransactionFailedException.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void findPersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + })); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void insertPersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailedException e) { + e.printStackTrace(); + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void replacePersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) + .one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void findPersonSpringTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + }); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + */ + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Bean + public Cluster couchbaseCluster() { + return Cluster.connect("10.144.220.101", "Administrator", "password"); + } + + /* + @Bean("personService") + PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + return new PersonService(ops, mgr, opsRx, mgrRx); + } + */ + + } + + @Data + // @AllArgsConstructor + static class EventLog { + public EventLog() {} + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + public EventLog(String id, String action) { + this.id = id; + this.action = action; + } + + String id; + String action; + @Version + Long version; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java index 3c6ddfd36..69b5536e1 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -21,7 +21,6 @@ import com.couchbase.client.java.transactions.TransactionResult; import com.couchbase.client.java.transactions.error.TransactionFailedException; -import org.junit.jupiter.api.Disabled; import reactor.core.publisher.Mono; import java.time.Duration; @@ -43,7 +42,7 @@ import org.springframework.data.couchbase.domain.ReactivePersonRepository; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; -import org.springframework.data.couchbase.transaction.CouchbaseStuffHandle; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -52,7 +51,6 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.reactive.TransactionalOperator; -import com.couchbase.client.core.cnc.Event; import com.couchbase.client.core.error.DocumentNotFoundException; import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; @@ -65,7 +63,7 @@ */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(CouchbaseReactiveTransactionNativeTests.Config.class) -@Disabled // Now using TransactionSyncronizationManager for the session +//@Disabled // Now using TransactionSyncronizationManager for the session public class CouchbaseReactiveTransactionNativeTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory couchbaseClientFactory; @@ -88,7 +86,9 @@ public static void beforeAll() { @AfterAll public static void afterAll() { callSuperAfterAll(new Object() {}); - context.close(); + if(context != null){ + context.close(); + } } @BeforeEach @@ -103,7 +103,7 @@ public void replacePersonTemplate() { remove(rxCBTmpl, cName, person.getId().toString()); rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator .reactive((ctx) -> ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) @@ -131,7 +131,7 @@ public void replacePersonRbTemplate() { remove(rxCBTmpl, cName, person.getId().toString()); rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); sleepMs(1000); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator .reactive((ctx) -> ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) @@ -157,7 +157,7 @@ public void insertPersonTemplate() { Person person = new Person(1, "Walter", "White"); remove(rxCBTmpl, cName, person.getId().toString()); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator.reactive((ctx) -> ctx.template(rxCBTmpl).insertById(Person.class) .one(person).flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) // .flatMap(it -> Mono.error(new PoofException())) @@ -183,7 +183,7 @@ public void insertPersonRbTemplate() { Person person = new Person(1, "Walter", "White"); remove(rxCBTmpl, cName, person.getId().toString()); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator.reactive((ctx) -> ctx.template(rxCBTmpl).insertById(Person.class) .one(person).flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) .flatMap(it -> Mono.error(new SimulateFailureException())).then()); @@ -200,7 +200,7 @@ public void insertPersonRbTemplate() { } // Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of "+SimulateFailureException.class); } @Test @@ -209,7 +209,7 @@ public void replacePersonRbRepo() { remove(rxCBTmpl, cName, person.getId().toString()); rxRepo.withCollection(cName).save(person).block(); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator .reactive((ctx) -> ctx.repository(rxRepo).withCollection(cName).findById(person.getId().toString()) .flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) @@ -235,7 +235,7 @@ public void insertPersonRbRepo() { Person person = new Person(1, "Walter", "White"); remove(rxCBTmpl, cName, person.getId().toString()); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator .reactive((ctx) -> ctx.repository(rxRepo).withTransaction(txOperator).withCollection(cName).save(person) // insert //.flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) // replace @@ -261,7 +261,7 @@ public void insertPersonRepo() { Person person = new Person(1, "Walter", "White"); remove(rxCBTmpl, cName, person.getId().toString()); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator .reactive((ctx) -> ctx.repository(rxRepo).withCollection(cName).save(person) // insert .flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) // replace @@ -327,8 +327,8 @@ public void findReplacePersonCBTransactionsRxTmpl() { Person person = new Person(1, "Walter", "White"); remove(rxCBTmpl, cName, person.getId().toString()); rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); - Mono result = txOperator.reactive((ctx) -> { + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive(ctx -> { rxCBTmpl.support().getTxResultHolder(person); return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(txOperator).one(person.getId().toString()) .flatMap(pGet -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(txOperator) @@ -345,7 +345,7 @@ public void insertReplacePersonsCBTransactionsRxTmpl() { Person person = new Person(1, "Walter", "White"); remove(rxCBTmpl, cName, person.getId().toString()); - CouchbaseStuffHandle txOperator = new CouchbaseStuffHandle(reactiveCouchbaseTransactionManager); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator.reactive((ctx) -> { return rxCBTmpl .insertById(Person.class).inCollection(cName).transaction(txOperator).one(person).flatMap(pInsert -> rxCBTmpl diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java deleted file mode 100644 index 7a6fcf809..000000000 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransaction2IntegrationTests.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2018-2021 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; -import static org.assertj.core.api.Assertions.assertThat; - -import com.couchbase.client.core.error.DocumentNotFoundException; -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.time.Duration; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.data.annotation.Id; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; -import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; -import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterType; -import org.springframework.data.couchbase.util.IgnoreWhen; -import org.springframework.data.couchbase.util.JavaIntegrationTests; -import org.springframework.data.domain.Persistable; -import org.springframework.test.annotation.Rollback; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.test.context.transaction.AfterTransaction; -import org.springframework.test.context.transaction.BeforeTransaction; -import org.springframework.transaction.annotation.Transactional; - -import com.couchbase.client.core.cnc.Event; -import com.example.demo.CouchbaseTransactionManager; -import com.example.demo.CouchbaseTransactionalTemplate; - -/** - * @author Christoph Strobl - * @currentRead Shadow's Edge - Brent Weeks - */ - -@ContextConfiguration -@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) -@SpringJUnitConfig(CouchbaseTemplateTransaction2IntegrationTests.Config.class) -public class CouchbaseTemplateTransaction2IntegrationTests extends JavaIntegrationTests { - - static final String DB_NAME = "template-tx-tests"; - static final String COLLECTION_NAME = "assassins"; - - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Bean - public CouchbaseTransactionManager transactionManager(@Autowired CouchbaseTemplate template) { - return new CouchbaseTransactionManager(template); - } - - @Bean - public CouchbaseTransactionalTemplate transactionalTemplate(CouchbaseTransactionManager manager) { - return manager.template(); - } - - } - - @Autowired CouchbaseTemplate template; - - List>> assertionList; - - @BeforeEach - public void setUp() { - - // template.setReadPreference(ReadPreference.primary()); - assertionList = new CopyOnWriteArrayList<>(); - } - - @BeforeTransaction - public void beforeTransaction() { - template.removeByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); - Collection a = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); - } - - @AfterTransaction - public void verifyDbState() { - - // Collection collection = template.getCollection("_default") ;// - // client.getDatabase(DB_NAME).getCollection(COLLECTION_NAME); - - Collection p = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); - System.out.println("assassins: " + p); - assertionList.forEach(it -> { - - boolean isPresent = template.findById(Assassin.class).one(it.getId().toString()) != null; // (Filters.eq("_id", - // it.getId())) != 0; - - assertThat(isPresent).isEqualTo(it.shouldBePresent()) - .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), - it.shouldBePresent() ? "be present" : "NOT be present")); - }); - } - - @Rollback(false) - @Transactional() - @Test // DATAMONGO-1920 - public void shouldOperateCommitCorrectly() { - - Assassin hu = new Assassin("hu", "Hu Gibbet"); - template.insertById(Assassin.class).one(hu); - - assertAfterTransaction(hu).isPresent(); - } - - @Test // DATAMONGO-1920 - // @Rollback(false) by default on these tests - public void shouldOperateRollbackCorrectly() { - - Assassin vi = new Assassin("vi", "Viridiana Sovari"); - try { - template.removeById(Assassin.class).one(vi.getId()); // could be something that is not an Assassin - } catch (DataRetrievalFailureException dnfe) {} - template.insertById(Assassin.class).one(vi); - assertAfterTransaction(vi).isNotPresent(); - } - - @Test // DATAMONGO-1920 - // @Rollback(false) by default on these tests - public void shouldBeAbleToViewChangesDuringTransaction() throws InterruptedException { - Assassin durzo = new Assassin("durzo", "Durzo Blint"); - template.insertById(Assassin.class).one(durzo); - Assassin retrieved = template.findById(Assassin.class).one(durzo.getId()); - assertThat(retrieved).isEqualTo(durzo); - assertAfterTransaction(durzo).isNotPresent(); - } - - // --- Just some helpers and tests entities - - private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) { - - AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(assassin); - assertionList.add(assertion); - return assertion; - } - - @Data - @AllArgsConstructor - @Document - static class Assassin implements Persistable { - - @Id String id; - String name; - - @Override - public boolean isNew() { - return id == null; - } - } -} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java index ffd19e851..6e0862d04 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java @@ -17,11 +17,10 @@ import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; -import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import com.couchbase.client.core.env.SecurityConfig; import com.couchbase.client.core.error.DocumentNotFoundException; -import com.couchbase.client.java.env.ClusterEnvironment; +import com.example.demo.CouchbaseTransactionalTemplate; import lombok.AllArgsConstructor; import lombok.Data; @@ -31,17 +30,22 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -49,20 +53,27 @@ import org.springframework.data.domain.Persistable; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.transaction.AfterTransaction; import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; import com.couchbase.client.core.cnc.Event; +//import com.example.demo.CouchbaseTransactionManager; +//import com.example.demo.CouchbaseTransactionalTemplate; /** * @author Christoph Strobl * @currentRead Shadow's Edge - Brent Weeks */ -// @ContextConfiguration - not needed?? + +//@ContextConfiguration +@ExtendWith({ SpringExtension.class }) @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +@Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) @SpringJUnitConfig(CouchbaseTemplateTransactionIntegrationTests.Config.class) public class CouchbaseTemplateTransactionIntegrationTests extends JavaIntegrationTests { @@ -72,6 +83,7 @@ public class CouchbaseTemplateTransactionIntegrationTests extends JavaIntegratio @Configuration @EnableCouchbaseRepositories("org.springframework.data.couchbase") @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + @EnableTransactionManagement static class Config extends AbstractCouchbaseConfiguration { @Override @@ -93,6 +105,17 @@ public String getPassword() { public String getBucketName() { return bucketName(); } + + @Bean + public CouchbaseTransactionManager transactionManager(@Autowired CouchbaseClientFactory template) { + return new CouchbaseTransactionManager(template, null); + } + + //@Bean + //public CouchbaseTransactionalTemplate transactionalTemplate(CouchbaseTransactionManager manager) { + // return manager.template(); + //} + } @Autowired CouchbaseTemplate template; @@ -101,7 +124,6 @@ public String getBucketName() { @BeforeEach public void setUp() { - // template.setReadPreference(ReadPreference.primary()); assertionList = new CopyOnWriteArrayList<>(); } @@ -109,15 +131,10 @@ public void setUp() { public void beforeTransaction() { template.removeByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); Collection a = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); - System.err.println("assassins before tx: " + a); } @AfterTransaction public void verifyDbState() { - - // Collection collection = template.getCollection("_default") ;// - // client.getDatabase(DB_NAME).getCollection(COLLECTION_NAME); - Collection p = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); System.out.println("assassins: " + p); assertionList.forEach(it -> { @@ -131,17 +148,22 @@ public void verifyDbState() { }); } + @Test @Rollback(false) - @Test // DATAMONGO-1920 + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) public void shouldOperateCommitCorrectly() { + assert(TestTransaction.isActive()); Assassin hu = new Assassin("hu", "Hu Gibbet"); template.insertById(Assassin.class).one(hu); assertAfterTransaction(hu).isPresent(); } - @Test // DATAMONGO-1920 - // @Rollback(false) by default on these tests + @Test + @Disabled // JCBC-1951 - run it twice second time fails. Recreate bucket, it succeeds + @Rollback(true) + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) public void shouldOperateRollbackCorrectly() { + assert(TestTransaction.isActive()); Assassin vi = new Assassin("vi", "Viridiana Sovari"); try { template.removeById(Assassin.class).one(vi.getId()); // could be something that is not an Assassin @@ -150,9 +172,12 @@ public void shouldOperateRollbackCorrectly() { assertAfterTransaction(vi).isNotPresent(); } - @Test // DATAMONGO-1920 - // @Rollback(false) by default on these tests - public void shouldBeAbleToViewChangesDuringTransaction() throws InterruptedException { + @Test + @Transactional(BeanNames.COUCHBASE_TRANSACTION_MANAGER) + @Disabled // JCBC-1951 - run it twice, second time fails. Recreate bucket, it succeeds + @Rollback(true) + public void shouldBeAbleToViewChangesDuringTransaction(){ + assert(TestTransaction.isActive()); Assassin durzo = new Assassin("durzo", "Durzo Blint"); template.insertById(Assassin.class).one(durzo); Assassin retrieved = template.findById(Assassin.class).one(durzo.getId()); @@ -163,7 +188,7 @@ public void shouldBeAbleToViewChangesDuringTransaction() throws InterruptedExcep // --- Just some helpers and tests entities private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) { - + assertInAnnotationTransaction(false); AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(assassin); assertionList.add(assertion); return assertion; diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java new file mode 100644 index 000000000..640b0c8c2 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java @@ -0,0 +1,266 @@ +/* + * Copyright 2012-2021 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Optional; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbaseTransactionNativeTests.Config.class) +public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { + + // @Autowired not supported on static fields. These are initialized in beforeAll() + // Also - @Autowired doesn't work here on couchbaseClientFactory even when it is not static, not sure why - oh, it + // seems there is not a ReactiveCouchbaseClientFactory bean + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired CouchbaseTransactionManager couchbaseTransactionManager; + + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + + @Autowired PersonRepository repo; + @Autowired ReactivePersonRepository repoRx; + @Autowired CouchbaseTemplate cbTmpl;; + static String cName; // short name + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + // short names + cName = null;// cName; + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @Test + public void replacePersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + + assertThrowsWithCause(() -> txOperator.run((ctx) -> { + Person p = txOperator.template(cbTmpl.reactive()).findById(Person.class).one(person.getId().toString()).block(); + Person pp = txOperator.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt")).block(); + throw new PoofException(); + }), TransactionFailedException.class, PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walter", pFound.getFirstname(), "firstname should be Walter"); + + } + + @Test + public void replacePersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause(() -> txOperator.run((ctx) -> { + Person p = ctx.template(cbTmpl.reactive()).findById(Person.class).one(person.getId().toString()).block(); + Person pp = ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt")).block(); + throw new PoofException(); + }), TransactionFailedException.class, PoofException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walter", pFound.getFirstname(), "firstname should be Walter"); + + } + + @Test + public void insertPersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + txOperator.reactive((ctx) -> ctx.template(cbTmpl.reactive()).insertById(Person.class).one(person) + .flatMap(p -> ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt"))).then()) + .block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + } + + @Test + public void insertPersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause( + () -> txOperator.reactive((ctx) -> ctx.template(cbTmpl.reactive()).insertById(Person.class).one(person) + .flatMap(p -> ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new PoofException()))).block(), + TransactionFailedException.class, PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void replacePersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + repo.withCollection(cName).save(person); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause(() -> txOperator.run(ctx -> { + ctx.repository(repoRx).withCollection(cName).findById(person.getId().toString()) + .flatMap(p -> ctx.repository(repoRx).withCollection(cName).save(p.withFirstName("Walt"))); + throw new PoofException(); + }), TransactionFailedException.class, PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + } + + @Test + public void insertPersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause(() -> txOperator.reactive((ctx) -> ctx.repository(repoRx).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(repoRx).withCollection(cName).save(p.withFirstName("Walt"))) // replace + .flatMap(it -> Mono.error(new PoofException()))).block(), TransactionFailedException.class, + PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void insertPersonRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + txOperator.reactive((ctx) -> ctx.repository(repoRx).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(repoRx).withCollection(cName).save(p.withFirstName("Walt"))) // replace + .then()).block(); + Optional pFound = repo.withCollection(cName).findById(person.getId().toString()); + assertEquals("Walt", pFound.get().getFirstname(), "firstname should be Walt"); + } + + @Test + public void replacePersonRbSpringTransactional() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause( + () -> txOperator + .reactive((ctx) -> ctx.template(cbTmpl.reactive()).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new PoofException()))) + .block(), + TransactionFailedException.class, PoofException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walter", pFound.getFirstname(), "firstname should be Walter"); + } + + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + static class PoofException extends RuntimeException {}; + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save deleted file mode 100644 index 894cf8594..000000000 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.save +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright 2012-2021 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 static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.time.Duration; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.domain.Person; -import org.springframework.data.couchbase.domain.PersonRepository; -import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; -import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; -import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; -import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; -import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; -import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterType; -import org.springframework.data.couchbase.util.IgnoreWhen; -import org.springframework.data.couchbase.util.JavaIntegrationTests; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import com.couchbase.client.core.cnc.Event; -import com.couchbase.client.core.error.DocumentNotFoundException; -import com.couchbase.client.java.Collection; -import com.couchbase.client.java.ReactiveCollection; -import com.couchbase.client.java.kv.RemoveOptions; -import com.couchbase.transactions.TransactionDurabilityLevel; -import com.couchbase.transactions.TransactionResult; -import com.couchbase.transactions.config.TransactionConfig; -import com.couchbase.transactions.config.TransactionConfigBuilder; -import com.couchbase.transactions.error.TransactionFailedException; - -/** - * Tests for com.couchbase.transactions without using the spring data transactions framework - * - * @author Michael Reiche - */ -@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -@SpringJUnitConfig(CouchbaseTransactionNativeTests.Config.class) -public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { - - // @Autowired not supported on static fields. These are initialized in beforeAll() - // Also - @Autowired doesn't work here on couchbaseClientFactory even when it is not static, not sure why - oh, it - // seems there is not a ReactiveCouchbaseClientFactory bean - @Autowired CouchbaseClientFactory couchbaseClientFactory; - @Autowired CouchbaseTransactionManager couchbaseTransactionManager; - - @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; - - @Autowired PersonRepository repo; - @Autowired CouchbaseTemplate cbTmpl;; - static String cName; // short name - - @BeforeAll - public static void beforeAll() { - callSuperBeforeAll(new Object() {}); - // short names - cName = null;// cName; - } - - @AfterAll - public static void afterAll() { - callSuperAfterAll(new Object() {}); - } - - @Test - public void replacePersonTemplate() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - - - try { - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( - reactiveCouchbaseTransactionManager); - txOperator.execute(reactiveTransaction -> { - return cbTmpl.replaceById(Person.class).one(person); - }); - - Flux result = txOperator.execute((ctx) -> { - Person p = txOperator.template(cbTmpl).findById(Person.class).one(person.getId().toString()); - txOperator.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt")); - // throw new PoofException(); - return null; - }); - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } - e.printStackTrace(); - } - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void replacePersonRbTemplate() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - try { - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( - reactiveCouchbaseTransactionManager); - TransactionResult result = txOperator - .execute((ctx) -> ctx.template(cbTmpl).findById(Person.class).one(person.getId().toString()) - .flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) - .flatMap(it -> Mono.error(new PoofException())).then()); - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } - e.printStackTrace(); - } - // Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void insertPersonTemplate() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - - try { - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( - reactiveCouchbaseTransactionManager); - Mono result = txOperator.reactive((ctx) -> ctx.template(cbTmpl).insertById(Person.class) - .one(person).flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) - // .flatMap(it -> Mono.error(new PoofException())) - .then()); - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should NOT have found " + pFound); - return; - } - e.printStackTrace(); - } - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void insertPersonRbTemplate() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - try { - - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( - reactiveCouchbaseTransactionManager); - Mono result = txOperator.reactive((ctx) -> ctx.template(cbTmpl).insertById(Person.class) - .one(person).flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) - .flatMap(it -> Mono.error(new PoofException())).then()); - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should NOT have found " + pFound); - return; - } - e.printStackTrace(); - } - // Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void replacePersonRbRepo() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - repo.withCollection(cName).save(person); - try { - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator( - reactiveCouchbaseTransactionManager); - Mono result = txOperator - .reactive((ctx) -> ctx.repository(repo).withCollection(cName).findById(person.getId().toString()) - .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) - .flatMap(it -> Mono.error(new PoofException())).then()); - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } - e.printStackTrace(); - } - // Person pFound = repo.withCollection(cName).findById(person.getId().toString()); - // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void insertPersonRbRepo() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); -try { - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(couchbaseTransactionManager); - Mono result = txOperator - .reactive((ctx) -> ctx.repository(repo).withCollection(cName).save(person) // insert - .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) // replace - .flatMap(it -> Mono.error(new PoofException())).then()); - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should NOT have found " + pFound); - return; - } - e.printStackTrace(); - } - // Person pFound = repo.withCollection(cName).findById(person.getId().toString()); - // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void insertPersonRepo() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); -try { - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); - Mono result = txOperator - .reactive((ctx) -> ctx.repository(repo).withCollection(cName).save(person) // insert - .flatMap(p -> ctx.repository(repo).withCollection(cName).save(p.withFirstName("Walt"))) // replace - // .flatMap(it -> Mono.error(new PoofException())) - .then()); - - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should NOT have found " + pFound); - return; - } - e.printStackTrace(); - } - Person pFound = repo.withCollection(cName).findById(person.getId().toString()); - assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - @Test - public void replacePersonRbSpringTransactional() { - Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); - cbTmpl.insertById(Person.class).inCollection(cName).one(person); - - try { - CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); - Mono result = txOperator.reactive((ctx) -> { - ctx.transactionResultHolder(123); - return ctx.template(cbTmpl).findById(Person.class).one(person.getId().toString()) - .flatMap(p -> ctx.template(cbTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) - .as(txOperator::transactional).then(); - }, false); - - } catch (TransactionFailedException e) { - if (e.getCause() instanceof PoofException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } - e.printStackTrace(); - } - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); - // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); - } - - void remove(Collection col, String id) { - remove(col.reactive(), id); - } - - void remove(ReactiveCollection col, String id) { - try { - col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))); - } catch (DocumentNotFoundException nfe) { - System.out.println(id + " : " + "DocumentNotFound when deleting"); - } - } - - void remove(CouchbaseTemplate template, String collection, String id) { - remove(template.reactive(), collection, id); - } - - void remove(ReactiveCouchbaseTemplate template, String collection, String id) { - try { - template.removeById(Person.class).inCollection(collection).one(id); - System.out.println("removed " + id); - } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { - System.out.println(id + " : " + "DocumentNotFound when deleting"); - } - } - - static class PoofException extends Exception {}; - - @Configuration - @EnableCouchbaseRepositories("org.springframework.data.couchbase") - @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") - static class Config extends AbstractCouchbaseConfiguration { - - @Override - public String getConnectionString() { - return connectionString(); - } - - @Override - public String getUserName() { - return config().adminUsername(); - } - - @Override - public String getPassword() { - return config().adminPassword(); - } - - @Override - public String getBucketName() { - return bucketName(); - } - - @Override - public TransactionConfig getTransactionConfig() { - return TransactionConfigBuilder.create().logDirectly(Event.Severity.INFO).logOnFailure(true, Event.Severity.ERROR) - .expirationTime(Duration.ofMinutes(10)).durabilityLevel(TransactionDurabilityLevel.MAJORITY).build(); - } - - } -} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceStandalone.java b/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceStandalone.java deleted file mode 100644 index 44025fb96..000000000 --- a/src/test/java/org/springframework/data/couchbase/transactions/PersonServiceStandalone.java +++ /dev/null @@ -1,244 +0,0 @@ -package org.springframework.data.couchbase.transactions; - -import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; - -import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.domain.Person; -import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; -import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.reactive.TransactionalOperator; -import org.springframework.transaction.support.DefaultTransactionDefinition; - -//@Service -class PersonServiceStandalone { - - final CouchbaseOperations personOperations; - final CouchbaseTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; - final ReactiveCouchbaseOperations personOperationsRx; - final ReactiveCouchbaseTransactionManager managerRx; - - public PersonServiceStandalone(CouchbaseOperations ops, CouchbaseTransactionManager mgr, ReactiveCouchbaseOperations opsRx, - ReactiveCouchbaseTransactionManager mgrRx) { - personOperations = ops; - manager = mgr; - System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); - System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); - System.err.println("manager Manager : " + manager); - - personOperationsRx = opsRx; - managerRx = mgrRx; - System.out - .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); - System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); - System.out.println("managerRx Manager : " + managerRx); - return; - } - - public Person savePersonErrors(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return personOperationsRx.insertById(Person.class).one(person)// - . flatMap(it -> Mono.error(new SimulateFailureException()))// - .as(transactionalOperator::transactional).block(); - } - - public Person savePerson(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - return personOperationsRx.insertById(Person.class).one(person)// - .as(transactionalOperator::transactional).block(); - } - - public Long countDuringTx(Person person) { - assertInAnnotationTransaction(false); - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return personOperationsRx.insertById(Person.class).one(person)// - .then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) - .as(transactionalOperator::transactional).block(); - } - - // @Transactional - public List saveWithLogs(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return Flux - .merge( - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeConvert")), - // - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterConvert")), - // - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeInsert")), - // - personOperationsRx.insertById(Person.class).one(person), - // - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) // - .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class).all()) // - .as(transactionalOperator::transactional).collectList().block(); - - } - - public List saveWithErrorLogs(Person person) { - assertInAnnotationTransaction(false); - TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, - new DefaultTransactionDefinition()); - - return Flux - .merge( - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeConvert")), - // - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterConvert")), - // - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeInsert")), - // - personOperationsRx.insertById(Person.class).one(person), - // - personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class) - .one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) // - .thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class).all()) // - . flatMap(it -> Mono.error(new SimulateFailureException())) - .as(transactionalOperator::transactional).collectList().block(); - - } - - // org.springframework.beans.factory.NoUniqueBeanDefinitionException: - // No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single - // matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) - public Person declarativeSavePerson(Person person) { - assertInAnnotationTransaction(true); - return personOperations.insertById(Person.class).one(person); - } - - public Person savePersonBlocking(Person person) { - if (1 == 1) - throw new RuntimeException("not implemented"); - assertInAnnotationTransaction(true); - return personOperations.insertById(Person.class).one(person); - - } - - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) - public Person declarativeSavePersonErrors(Person person) { - assertInAnnotationTransaction(true); - Person p = personOperations.insertById(Person.class).one(person); // - SimulateFailureException.throwEx(); - return p; - } - - @Autowired - CouchbaseSimpleCallbackTransactionManager callbackTm; - - /** - * to execute while ThreadReplaceloop() is running should force a retry - * - * The intention was to provide a template from the transaction manager (?) - * @param person - * @return - */ - /* - @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) - public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { - assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); - System.err.println("declarativeFindReplacePersonCallback cluster : " - + callbackTm.template().getCouchbaseClientFactory().getCluster().block()); - System.err.println("declarativeFindReplacePersonCallback resourceHolder : " - + org.springframework.transaction.support.TransactionSynchronizationManager - .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); - Person p = personOperations.findById(Person.class).one(person.getId().toString()); - return personOperations.replaceById(Person.class).one(p); - } -*/ - - /** - * to execute while ThreadReplaceloop() is running should force a retry - * - * @param person - * @return - */ - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) - public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { - assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePersonReactive try: " + tryCount.incrementAndGet()); - /* NoTransactionInContextException - TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { - System.err.println("declarativeFindReplacePersonReactive reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); - return Mono.just(sm); - }).block(); - */ - return personOperationsRx.findById(Person.class).one(person.getId().toString()) - .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p)); - } - - /** - * to execute while ThreadReplaceloop() is running should force a retry - * - * @param person - * @return - */ - @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) // doesn't retry - public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { - assertInAnnotationTransaction(true); - System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet()); - Person p = personOperations.findById(Person.class).one(person.getId().toString()); - return personOperations.replaceById(Person.class).one(p); - } - - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // doesn't retry - public Mono declarativeSavePersonReactive(Person person) { - assertInAnnotationTransaction(true); - return personOperationsRx.insertById(Person.class).one(person); - } - - @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) - public Mono declarativeSavePersonErrorsReactive(Person person) { - assertInAnnotationTransaction(true); - Mono p = personOperationsRx.insertById(Person.class).one(person); // - SimulateFailureException.throwEx(); - return p; - } - - void assertInAnnotationTransaction(boolean inTransaction) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - for (StackTraceElement ste : stack) { - if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") - || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { - if (inTransaction) { - return; - } - } - } - if (!inTransaction) { - return; - } - throw new RuntimeException("in_annotation_transaction = " + (!inTransaction) - + " but expected in_annotation_transaction = " + inTransaction + "\n class: " + getClass().getName()); - } - -} diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index dae0530bd..991fecefe 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -427,4 +427,27 @@ public static Throwable assertThrowsOneOf(Executable executable, Class... exp String message ="Expected "+expectedTypes+" to be thrown, but nothing was thrown."; throw new AssertionFailedError(message); } + + public static Throwable assertThrowsWithCause(Executable executable, Class... expectedTypes) { + Class nextExpectedException= null; + try { + executable.execute(); + } + catch (Throwable actualException) { + for(Class expectedType:expectedTypes){ + nextExpectedException = expectedType; + if(actualException == null || !expectedType.isAssignableFrom( actualException.getClass())){ + String message ="Expected "+nextExpectedException+" to be thrown/cause, but found "+actualException; + throw new AssertionFailedError(message, actualException); + } + actualException = actualException.getCause(); + } + //UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + return actualException; + } + + String message ="Expected "+expectedTypes[0]+" to be thrown, but nothing was thrown."; + throw new AssertionFailedError(message); + } + } diff --git a/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java index 1eda812a6..d77693645 100644 --- a/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java @@ -60,7 +60,7 @@ ClusterType type() { @Override TestClusterConfig _start() throws Exception { - bucketname = UUID.randomUUID().toString(); + bucketname = "my_bucket"; //UUID.randomUUID().toString(); Response postResponse = httpClient .newCall(new Request.Builder().header("Authorization", Credentials.basic(adminUsername, adminPassword)) diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java index 06710bb90..9e30569a7 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Util.java +++ b/src/test/java/org/springframework/data/couchbase/util/Util.java @@ -96,4 +96,25 @@ public static String readResource(final String filename, final Class clazz) { return s.hasNext() ? s.next() : ""; } + /** + * check if we are/are not in an @Transactional transaction + * @param inTransaction + */ + public static void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") + || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException( + "in-annotation-transaction = " + (!inTransaction) + " but expected in-annotation-transaction = " + inTransaction); + } + } From 9e7fddefc1bf04eff475c33a204f390d389f9258 Mon Sep 17 00:00:00 2001 From: Michael Reiche Date: Thu, 26 May 2022 14:33:48 -0700 Subject: [PATCH 8/9] Add ReactiveTransactionWrapper/TransactionWrapper and a bunch of cleanup. --- .../AttemptContextReactiveAccessor.java | 82 ++- .../ReactiveCouchbaseClientFactory.java | 23 +- .../SimpleReactiveCouchbaseClientFactory.java | 43 ++ .../ReactiveExistsByIdOperationSupport.java | 2 +- ...activeFindByAnalyticsOperationSupport.java | 4 +- .../ReactiveFindByIdOperationSupport.java | 12 +- .../ReactiveFindByQueryOperationSupport.java | 16 +- ...eFindFromReplicasByIdOperationSupport.java | 2 +- .../ReactiveRemoveByIdOperationSupport.java | 15 +- ...ReactiveRemoveByQueryOperationSupport.java | 4 +- .../ReactiveReplaceByIdOperationSupport.java | 15 +- .../ReactiveUpsertByIdOperationSupport.java | 2 +- .../CouchbaseTransactionManager.java | 72 +-- .../CouchbaseTransactionalOperator.java | 2 +- .../ReactiveCouchbaseTransactionManager.java | 18 +- .../ReactiveTransactionsWrapper.java | 71 +++ .../transaction/TransactionsWrapper.java | 225 +++----- ...basePersonTransactionIntegrationTests.java | 513 +++++++----------- ...uchbaseReactiveTransactionNativeTests.java | 1 - .../couchbase/util/JavaIntegrationTests.java | 30 +- 20 files changed, 498 insertions(+), 654 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ReactiveTransactionsWrapper.java diff --git a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java index 0909f32e9..15dfe35f5 100644 --- a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java +++ b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java @@ -22,6 +22,8 @@ import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; +import java.util.logging.Logger; import com.couchbase.client.core.annotation.Stability; import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; @@ -33,6 +35,7 @@ import com.couchbase.client.core.transaction.support.AttemptState; import com.couchbase.client.java.codec.JsonSerializer; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; /** * To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext @@ -66,13 +69,7 @@ public static ReactiveTransactionAttemptContext reactive(TransactionAttemptConte } catch (Throwable err) { throw new RuntimeException(err); } - try { - Field field = TransactionAttemptContext.class.getDeclaredField("internal"); - field.setAccessible(true); - return new ReactiveTransactionAttemptContext((CoreTransactionAttemptContext) field.get(atr), serializer); - } catch (Throwable err) { - throw new RuntimeException(err); - } + return new ReactiveTransactionAttemptContext(getCore(atr), serializer); } public static TransactionAttemptContext blocking(ReactiveTransactionAttemptContext atr) { @@ -84,35 +81,22 @@ public static TransactionAttemptContext blocking(ReactiveTransactionAttemptConte } catch (Throwable err) { throw new RuntimeException(err); } - try { - Field field = ReactiveTransactionAttemptContext.class.getDeclaredField("internal"); - field.setAccessible(true); - return new TransactionAttemptContext((CoreTransactionAttemptContext) field.get(atr), serializer); - } catch (Throwable err) { - throw new RuntimeException(err); - } + return new TransactionAttemptContext(getCore(atr), serializer); } public static CoreTransactionLogger getLogger(ReactiveTransactionAttemptContext attemptContextReactive) { - // todo gp needed? - return null; - // return attemptContextReactive; + return attemptContextReactive.logger(); + } + + public static CoreTransactionLogger getLogger(TransactionAttemptContext attemptContextReactive) { + return attemptContextReactive.logger(); } // todo gp needed? @Stability.Internal public static CoreTransactionAttemptContext newCoreTranactionAttemptContext(ReactiveTransactions transactions) { - // PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); - // MergedTransactionConfig merged = new MergedTransactionConfig(transactions.config(), Optional.of(perConfig)); - // - // TransactionContext overall = new TransactionContext( - // transactions.cleanup().clusterData().cluster().environment().requestTracer(), - // transactions.cleanup().clusterData().cluster().environment().eventBus(), - // UUID.randomUUID().toString(), now(), Duration.ZERO, merged); String txnId = UUID.randomUUID().toString(); - // overall.LOGGER.info(configDebug(transactions.config(), perConfig)); - CoreTransactionsReactive coreTransactionsReactive; try { Field field = ReactiveTransactions.class.getDeclaredField("internal"); @@ -122,12 +106,8 @@ public static CoreTransactionAttemptContext newCoreTranactionAttemptContext(Reac throw new RuntimeException(err); } - CoreTransactionOptions perConfig = new CoreTransactionOptions(Optional.empty(), - Optional.empty(), - Optional.empty(), - Optional.of(Duration.ofMinutes(10)), - Optional.empty(), - Optional.empty()); + CoreTransactionOptions perConfig = new CoreTransactionOptions(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.of(Duration.ofMinutes(10)), Optional.empty(), Optional.empty()); CoreMergedTransactionConfig merged = new CoreMergedTransactionConfig(coreTransactionsReactive.config(), Optional.ofNullable(perConfig)); @@ -135,14 +115,10 @@ public static CoreTransactionAttemptContext newCoreTranactionAttemptContext(Reac coreTransactionsReactive.core().context().environment().requestTracer(), coreTransactionsReactive.core().context().environment().eventBus(), UUID.randomUUID().toString(), merged, coreTransactionsReactive.core().transactionsCleanup()); - // overall.LOGGER.info(configDebug(config, perConfig, cleanup.clusterData().cluster().core())); CoreTransactionAttemptContext coreTransactionAttemptContext = coreTransactionsReactive.createAttemptContext(overall, merged, txnId); return coreTransactionAttemptContext; - // ReactiveTransactionAttemptContext reactiveTransactionAttemptContext = new ReactiveTransactionAttemptContext( - // coreTransactionAttemptContext, null); - // return reactiveTransactionAttemptContext; } private static Duration now() { @@ -168,15 +144,13 @@ public static CoreTransactionAttemptContext getCore(ReactiveTransactionAttemptCo } public static CoreTransactionAttemptContext getCore(TransactionAttemptContext atr) { - CoreTransactionAttemptContext coreTransactionsReactive; try { Field field = TransactionAttemptContext.class.getDeclaredField("internal"); field.setAccessible(true); - coreTransactionsReactive = (CoreTransactionAttemptContext) field.get(atr); + return (CoreTransactionAttemptContext) field.get(atr); } catch (Throwable err) { throw new RuntimeException(err); } - return coreTransactionsReactive; } public static Mono implicitCommit(ReactiveTransactionAttemptContext atr, boolean b) { @@ -186,17 +160,17 @@ public static Mono implicitCommit(ReactiveTransactionAttemptContext atr, b // CoreTransactionAttemptContext.class.getDeclaredMethod("implicitCommit", Boolean.class); Method[] methods = CoreTransactionAttemptContext.class.getDeclaredMethods(); Method method = null; - for(Method m:methods){ - if( m.getName().equals("implicitCommit")){ + for (Method m : methods) { + if (m.getName().equals("implicitCommit")) { method = m; break; } } - if(method == null){ + if (method == null) { throw new RuntimeException("did not find implicitCommit method"); } method.setAccessible(true); - return (Mono)method.invoke(coreTransactionsReactive, b); + return (Mono) method.invoke(coreTransactionsReactive, b); } catch (Throwable err) { throw new RuntimeException(err); } @@ -214,10 +188,30 @@ public static AttemptState getState(ReactiveTransactionAttemptContext atr) { } } - public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext(CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) { + public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext( + CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) { return new ReactiveTransactionAttemptContext(core, jsonSerializer); } + public static CoreTransactionsReactive getCoreTransactionsReactive(ReactiveTransactions transactions) { + try { + Field field = ReactiveTransactions.class.getDeclaredField("internal"); + field.setAccessible(true); + return (CoreTransactionsReactive) field.get(transactions); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static TransactionAttemptContext newTransactionAttemptContext(CoreTransactionAttemptContext ctx, + JsonSerializer jsonSerializer) { + return new TransactionAttemptContext(ctx, jsonSerializer); + } + + public static TransactionResult run(Transactions transactions, Consumer transactionLogic, CoreTransactionOptions coreTransactionOptions) { + return reactive(transactions).runBlocking(transactionLogic, coreTransactionOptions); + } + // todo gp if needed let's expose in the SDK // static private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { // StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java index 6d4372223..75a7acd2f 100644 --- a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java @@ -32,7 +32,6 @@ import java.io.IOException; - /** * Interface for factories creating reactive {@link Cluster} instances. * @@ -42,12 +41,17 @@ * @since 2.0 */ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider*/ { - + /** * Provides access to the managed SDK {@link Cluster} reference. */ Mono getCluster(); + /** + * Provides access to the managed SDK {@link Cluster} reference. + */ + ClusterInterface getBlockingCluster(); + /** * Provides access to the managed SDK {@link Bucket} reference. */ @@ -58,6 +62,11 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* */ Mono getScope(); + /** + * Provides access to the managed SDK {@link Scope} reference without block() + */ + Scope getBlockingScope(String scopeName); + /** * Provides access to a collection (identified by its name) in managed SDK {@link Scope} reference. * @@ -65,6 +74,13 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* */ Mono getCollection(String name); + /** + * Provides access to a collection (identified by its name) without block() + * + * @param name the name of the collection. If null is passed in, the default collection is assumed. + */ + Collection getBlockingCollection(String collectionName); + /** * Provides access to the default collection. */ @@ -91,7 +107,8 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* void close() throws IOException; - ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, CoreTransactionAttemptContext ctx); + ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, + CoreTransactionAttemptContext ctx); /* * (non-Javadoc) diff --git a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java index e114a1d69..ffd20b85a 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java @@ -29,6 +29,7 @@ public class SimpleReactiveCouchbaseClientFactory implements ReactiveCouchbaseClientFactory { final Mono cluster; + final ClusterInterface theCluster; final String bucketName; final String scopeName; final PersistenceExceptionTranslator exceptionTranslator; @@ -39,6 +40,7 @@ public class SimpleReactiveCouchbaseClientFactory implements ReactiveCouchbaseCl public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName, CouchbaseTransactionalOperator transactionalOperator) { this.cluster = Mono.just(cluster); + this.theCluster = cluster; this.bucketName = bucketName; this.scopeName = scopeName; this.exceptionTranslator = new CouchbaseExceptionTranslator(); @@ -56,6 +58,12 @@ public Mono getCluster() { return cluster; } + + @Override + public ClusterInterface getBlockingCluster() { + return theCluster; + } + @Override public Mono getBucket() { return cluster.map((c) -> c.bucket(bucketName)); @@ -71,6 +79,11 @@ public Mono getScope() { return cluster.map((c) -> c.bucket(bucketName).scope(scopeName != null ? scopeName : DEFAULT_SCOPE)); } + @Override + public Scope getBlockingScope(String scopeName) { + return theCluster.bucket(bucketName).scope(scopeName != null ? scopeName : (this.scopeName != null ? this.scopeName : DEFAULT_SCOPE)); + } + @Override public String getScopeName() { return scopeName; @@ -92,6 +105,22 @@ public Mono getCollection(String collectionName) { return getScope().map((s) -> s.collection(collectionName != null ? collectionName : DEFAULT_COLLECTION)); } + @Override + public Collection getBlockingCollection(String collectionName) { + if (getScopeName() != null && !DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName == null || DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used."); + } + } + if (getScopeName() == null || DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName != null && !DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException( + "A collectionName must be null or " + DEFAULT_COLLECTION + " if scope is null or " + DEFAULT_SCOPE); + } + } + return theCluster.bucket(bucketName).scope(scopeName != null ? scopeName : DEFAULT_SCOPE).collection(collectionName != null ? collectionName : DEFAULT_COLLECTION); + } + @Override public Mono getDefaultCollection() { if (getScopeName() != null && DEFAULT_SCOPE.equals(getScopeName())) { @@ -204,6 +233,11 @@ public Mono getCluster() throws DataAccessException { return delegate.getCluster().map(this::decorateDatabase); } + @Override + public ClusterInterface getBlockingCluster() throws DataAccessException { + return decorateDatabase(delegate.getBlockingCluster()); + } + @Override public Mono getBucket() { return delegate.getBucket(); @@ -219,6 +253,15 @@ public Mono getCollection(String name) { return delegate.getCollection(name); } + @Override + public Collection getBlockingCollection(String collectionName) { + return delegate.getBlockingCollection(collectionName); + } + + @Override + public Scope getBlockingScope(String scopeName) { + return delegate.getBlockingScope(scopeName); + } @Override public Mono getDefaultCollection() { return delegate.getDefaultCollection(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index 2ee4ba153..17eab98df 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -75,7 +75,7 @@ public Mono one(final String id) { LOG.trace("existsById {}", pArgs); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).block().reactive().exists(id, buildOptions(pArgs.getOptions())) + .getBlockingCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) .map(ExistsResult::exists)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index 3d8955edd..e429d0d19 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -109,7 +109,7 @@ public Mono first() { public Flux all() { return Flux.defer(() -> { String statement = assembleEntityQuery(false); - return template.getCouchbaseClientFactory().getCluster().block().reactive() + return template.getCouchbaseClientFactory().getBlockingCluster().reactive() .analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -142,7 +142,7 @@ public Flux all() { public Mono count() { return Mono.defer(() -> { String statement = assembleEntityQuery(true); - return template.getCouchbaseClientFactory().getCluster().block().reactive() + return template.getCouchbaseClientFactory().getBlockingCluster().reactive() .analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 785ca8275..e7e3afb3f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -92,7 +92,7 @@ public Mono one(final String id) { LOG.trace("findById {}", pArgs); ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).block().reactive(); + .getBlockingCollection(pArgs.getCollection()).reactive(); // this will get me a template with a session holding tx Mono tmpl = template.doGetTemplate(); @@ -118,16 +118,6 @@ public Mono one(final String id) { // todo gp no cas // todo mr - it's required by replace().one when comparing to internal.cas(). it's gone // todo gp if we need this of course needs to be exposed nicely Long cas = result.cas(); - /* - try { - Method method = TransactionGetResult.class.getDeclaredMethod("internal"); - method.setAccessible(true); - CoreTransactionGetResult internal = (CoreTransactionGetResult) method.invoke(result); - cas = internal.cas(); - } catch (Throwable err) { - throw new RuntimeException(err); - } -*/ return support.decodeEntity(id, new String(result.contentAsBytes()), cas, domainType, pArgs.getScope(), pArgs.getCollection(), new TransactionResultHolder(result), null).doOnNext(out -> { // todo gp is this safe? are we on the right thread? diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 05cd86781..95d027609 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -190,18 +190,18 @@ public Flux all() { LOG.trace("findByQuery {} statement: {}", pArgs, statement); ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); + ReactiveScope rs = clientFactory.getBlockingScope(pArgs.getScope()).reactive(); Mono tmpl = template.doGetTemplate(); Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { if (s.getCore() == null) { QueryOptions opts = buildOptions(pArgs.getOptions()); - return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + return pArgs.getScope() == null ? clientFactory.getBlockingCluster().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), - clientFactory.getCluster().block().environment().jsonSerializer())).query(statement, opts); + return AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getBlockingCluster().environment().jsonSerializer()).query(statement, opts); } })); @@ -253,18 +253,18 @@ public Mono count() { LOG.trace("findByQuery {} statement: {}", pArgs, statement); ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); + ReactiveScope rs = clientFactory.getBlockingScope(pArgs.getScope()).reactive(); Mono tmpl = template.doGetTemplate(); Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { if (s.getCore() == null) { QueryOptions opts = buildOptions(pArgs.getOptions()); - return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + return pArgs.getScope() == null ? clientFactory.getBlockingCluster().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), - clientFactory.getCluster().block().environment().jsonSerializer())).query(statement, opts); + return AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getBlockingCluster().environment().jsonSerializer()).query(statement, opts); } })); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 00760c6c7..1be069939 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -77,7 +77,7 @@ public Mono any(final String id) { LOG.trace("getAnyReplica {}", pArgs); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).block().reactive().getAnyReplica(docId, pArgs.getOptions())) + .getBlockingCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index eb03491f7..15489c3f1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,7 +15,9 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.error.CasMismatchException; import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import com.couchbase.client.core.transaction.CoreTransactionGetResult; import com.couchbase.client.java.transactions.TransactionGetResult; import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; @@ -116,21 +118,10 @@ public Mono one(final String id) { // todo gp no CAS return gr.flatMap(getResult -> { - /* - CoreTransactionGetResult internal; - try { - Method method = CoreTransactionGetResult.class.getDeclaredMethod("internal"); - method.setAccessible(true); - internal = (CoreTransactionGetResult) method.invoke(getResult); - } - catch (Throwable err) { - throw new RuntimeException(err); - } -*/ if (getResult.cas() != cas) { System.err.println("internal: "+getResult.cas()+" object.cas: "+cas); // todo gp really want to set internal state and raise a TransactionOperationFailed - throw new RetryTransactionException(); + return Mono.error(new TransactionOperationFailedException(true, true, new CasMismatchException(null), null)); } return s.getCore().remove(getResult) .map(r -> new RemoveResult(id, 0, null)); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index d5f5e947c..5d08448a5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -83,10 +83,10 @@ public Flux all() { LOG.trace("removeByQuery {} statement: {}", pArgs, statement); Mono allResult = null; ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); - ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().block().reactive(); + ReactiveScope rs = clientFactory.getBlockingScope(pArgs.getScope()).reactive(); if (pArgs.getTxOp() == null) { QueryOptions opts = buildQueryOptions(pArgs.getOptions()); - allResult = pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + allResult = pArgs.getScope() == null ? clientFactory.getBlockingCluster().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(buildQueryOptions(pArgs.getOptions())); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index fa4288836..f03e54d14 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -15,7 +15,9 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.error.CasMismatchException; import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import com.couchbase.client.core.transaction.CoreTransactionGetResult; import com.couchbase.client.java.transactions.TransactionGetResult; import reactor.core.publisher.Flux; @@ -130,21 +132,10 @@ public Mono one(T object) { // todo gp no CAS return gr.flatMap(getResult -> { -/* - CoreTransactionGetResult internal; - try { - Method method = TransactionGetResult.class.getDeclaredMethod("internal"); - method.setAccessible(true); - internal = (CoreTransactionGetResult) method.invoke(getResult); - } - catch (Throwable err) { - throw new RuntimeException(err); - } -*/ if (getResult.cas() != support.cas) { System.err.println("internal: "+getResult.cas()+" object.cas: "+ support.cas+" "+converted); // todo gp really want to set internal state and raise a TransactionOperationFailed - throw new RetryTransactionException(); + return Mono.error(new TransactionOperationFailedException(true, true, new CasMismatchException(null), null)); } return support.ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().block().environment().transcoder() .encode(support.converted.export()).encoded()); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index cd7e9a82a..fd0c3b5f0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -85,7 +85,7 @@ public Mono one(T object) { Mono tmpl = template.doGetTemplate(); Mono reactiveEntity = support.encodeEntity(object) .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { - if (s == null ) { + if (s.getCore() == null) { return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).flatMap(collection -> collection.reactive() .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java index a1490e5dc..9a72c7e1e 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java @@ -45,9 +45,10 @@ *

    * Binds a {@link CoreTransactionAttemptContext} from the specified {@link CouchbaseClientFactory} to the thread. *

    - * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link CoreTransactionAttemptContext} and enable causal - * consistency, and also {@link CoreTransactionAttemptContext#startTransaction() start}, {@link CoreTransactionAttemptContext#commitTransaction() - * commit} or {@link CoreTransactionAttemptContext#abortTransaction() abort} a transaction. + * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link CoreTransactionAttemptContext} + * and enable causal consistency, and also {@link CoreTransactionAttemptContext#startTransaction() start}, + * {@link CoreTransactionAttemptContext#commitTransaction() commit} or + * {@link CoreTransactionAttemptContext#abortTransaction() abort} a transaction. *

    * TODO: Application code is required to retrieve the {@link com.couchbase.client.java.Cluster} ????? via * {@link ?????#getDatabase(CouchbaseClientFactory)} instead of a standard {@link CouchbaseClientFactory#getCluster()} @@ -101,7 +102,7 @@ public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory, @Null System.err.println(databaseFactory.getCluster()); this.databaseFactory = databaseFactory; this.options = options; - this.transactions = databaseFactory.getCluster().transactions(); + this.transactions = databaseFactory.getCluster().transactions(); } /* @@ -132,14 +133,15 @@ protected boolean isExistingTransaction(Object transaction) throws TransactionEx protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); -// should ACR already be in TSM? TransactionSynchronizationManager.bindResource(getRequiredDbFactory().getCluster(), resourceHolder); - ReactiveCouchbaseResourceHolder resourceHolder = newResourceHolder(definition, TransactionOptions.transactionOptions(), + // should ACR already be in TSM? TransactionSynchronizationManager.bindResource(getRequiredDbFactory().getCluster(), + // resourceHolder); + ReactiveCouchbaseResourceHolder resourceHolder = newResourceHolder(getDatabaseFactory(), definition, + TransactionOptions.transactionOptions(), null /* ((CouchbaseTransactionDefinition) definition).getAttemptContextReactive()*/); couchbaseTransactionObject.setResourceHolder(resourceHolder); if (logger.isDebugEnabled()) { - logger - .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); + logger.debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); } try { @@ -154,14 +156,10 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr } TransactionSynchronizationManager.setActualTransactionActive(true); - // use the ResourceHolder which contains the core - //TransactionSynchronizationManager.unbindResourceIfPossible(TransactionAttemptContext.class); - //TransactionSynchronizationManager.bindResource(CoreTransactionAttemptContext.class, resourceHolder.getCore()); - resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.unbindResourceIfPossible( getRequiredDatabaseFactory().getCluster()); - System.err.println("CouchbaseTransactionManager: "+this); - System.err.println("bindResource: "+ getRequiredDatabaseFactory().getCluster()+" value: "+resourceHolder); + TransactionSynchronizationManager.unbindResourceIfPossible(getRequiredDatabaseFactory().getCluster()); + System.err.println("CouchbaseTransactionManager: " + this); + System.err.println("bindResource: " + getRequiredDatabaseFactory().getCluster() + " value: " + resourceHolder); TransactionSynchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster(), resourceHolder); } @@ -204,7 +202,8 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio try { doCommit(couchbaseTransactionObject); } catch (Exception ex) { - logger.debug("could not commit Couchbase transaction for session "+debugString(couchbaseTransactionObject.getCore())); + logger.debug( + "could not commit Couchbase transaction for session " + debugString(couchbaseTransactionObject.getCore())); throw new TransactionSystemException(String.format("Could not commit Couchbase transaction for session %s.", debugString(couchbaseTransactionObject.getCore())), ex); } @@ -214,9 +213,8 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio * Customization hook to perform an actual commit of the given transaction.
    * If a commit operation encounters an error, the MongoDB driver throws a {@link CouchbaseException} holding * {@literal error labels}.
    - * By default those labels are ignored, nevertheless one might check for - * {@link CouchbaseException transient commit errors labels} and retry the the - * commit.
    + * By default those labels are ignored, nevertheless one might check for {@link CouchbaseException transient commit + * errors labels} and retry the the commit.
    * *

     	 * int retries = 3;
    @@ -272,8 +270,8 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc
     	protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException {
     
     		CouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status);
    -		throw new TransactionException("need to setRollbackOnly() here"){};
    -		//transactionObject.getRequiredResourceHolder().setRollbackOnly();
    +		throw new TransactionException("need to setRollbackOnly() here") {};
    +		// transactionObject.getRequiredResourceHolder().setRollbackOnly();
     	}
     
     	/*
    @@ -291,7 +289,7 @@ protected void doCleanupAfterCompletion(Object transaction) {
     
     		// Remove the connection holder from the thread.
     		TransactionSynchronizationManager.unbindResourceIfPossible(getRequiredDatabaseFactory().getCluster());
    -		//couchbaseTransactionObject.getRequiredResourceHolder().clear();
    +		// couchbaseTransactionObject.getRequiredResourceHolder().clear();
     
     		if (logger.isDebugEnabled()) {
     			logger.debug(String.format("About to release Core %s after transaction.",
    @@ -349,15 +347,10 @@ public void afterPropertiesSet() {
     		getRequiredDatabaseFactory();
     	}
     
    -	private ReactiveCouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, TransactionOptions options,
    -			CoreTransactionAttemptContext atr) {
    -
    -		CouchbaseClientFactory databaseFactory = getResourceFactory();
    -
    +	static ReactiveCouchbaseResourceHolder newResourceHolder(CouchbaseClientFactory databaseFactory, TransactionDefinition definition,
    +			TransactionOptions options, CoreTransactionAttemptContext atr) {
     		ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(
     				databaseFactory.getCore(options, atr));
    -		// TODO resourceHolder.setTimeoutIfNotDefaulted(determineTimeout(definition));
    -
     		return resourceHolder;
     	}
     
    @@ -390,23 +383,18 @@ private static CouchbaseTransactionObject extractCouchbaseTransaction(DefaultTra
     		return (CouchbaseTransactionObject) status.getTransaction();
     	}
     
    -	private static String debugString(@Nullable CoreTransactionAttemptContext session) {
    -
    -		if (session == null) {
    +	 static String debugString(@Nullable CoreTransactionAttemptContext ctx) {
    +		if (ctx == null) {
     			return "null";
     		}
    -
    -		String debugString = String.format("[%s@%s ", ClassUtils.getShortName(session.getClass()),
    -				Integer.toHexString(session.hashCode()));
    -
    +		String debugString = String.format("[%s@%s ", ClassUtils.getShortName(ctx.getClass()),
    +				Integer.toHexString(ctx.hashCode()));
     		try {
    -			debugString += String.format("core=%s",session);
    +			debugString += String.format("core=%s", ctx);
     		} catch (RuntimeException e) {
     			debugString += String.format("error = %s", e.getMessage());
     		}
    -
     		debugString += "]";
    -
     		return debugString;
     	}
     
    @@ -415,8 +403,8 @@ public CouchbaseClientFactory getDatabaseFactory() {
     	}
     
     	/**
    -	 * MongoDB specific transaction object, representing a {@link ReactiveCouchbaseResourceHolder}. Used as transaction object by
    -	 * {@link CouchbaseTransactionManager}.
    +	 * MongoDB specific transaction object, representing a {@link ReactiveCouchbaseResourceHolder}. Used as transaction
    +	 * object by {@link CouchbaseTransactionManager}.
     	 *
     	 * @author Christoph Strobl
     	 * @author Mark Paluch
    @@ -458,7 +446,7 @@ void startTransaction(TransactionOptions options) {
     			// if (options != null) {
     			// session.startTransaction(options);
     			// } else {
    -			//core.startTransaction();
    +			// core.startTransaction();
     			// }
     		}
     
    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 12e967ea0..10002be95 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java
    @@ -193,7 +193,7 @@ public  Mono transactional(Mono mono) {
     			// through usingWhen.
     			return status
     					.flatMap(it -> Mono
    -							.usingWhen(Mono.just(it), ignore -> mono, this.transactionManager::commit, (res, err) -> Mono.empty(),
    +							.usingWhen(Mono.just(it), ignore -> mono, this.transactionManager::commit, (res, err) -> { System.err.println("!!!!!!!!!! "+err+" "+res); return Mono.empty();},
     									this.transactionManager::rollback)
     							.onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex))));
     		}).contextWrite(TransactionContextManager.getOrCreateContext())
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java
    index 90ebf249c..86d2e5535 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java
    @@ -98,15 +98,6 @@ public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databa
     		System.err.println("ReactiveCouchbaseTransactionManager : created");
     	}
     
    -	public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databaseFactory,
    -			@Nullable Transactions transactions) {
    -		Assert.notNull(databaseFactory, "DatabaseFactory must not be null!");
    -		this.databaseFactory = databaseFactory; // databaseFactory; // should be a clone? TransactionSynchronizationManager
    -																						// binds objs to it
    -		this.transactions = transactions;
    -		System.err.println("ReactiveCouchbaseTransactionManager : created Transactions: " + transactions);
    -	}
    -
     	/*
     	 * (non-Javadoc)
     	 * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doGetTransaction(org.springframework.transaction.reactive.TransactionSynchronizationManager)
    @@ -117,9 +108,7 @@ protected Object doGetTransaction(TransactionSynchronizationManager synchronizat
     		// creation of a new ReactiveCouchbaseTransactionObject (i.e. transaction).
     		// with an attempt to get the resourceHolder from the synchronizationManager
     		ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager
    -				.getResource(getRequiredDatabaseFactory().getCluster().block());
    -		// TODO ACR from couchbase
    -		// resourceHolder.getSession().setAttemptContextReactive(null);
    +				.getResource(getRequiredDatabaseFactory().getBlockingCluster());
     		return new ReactiveCouchbaseTransactionObject(resourceHolder);
     	}
     
    @@ -151,20 +140,15 @@ protected Mono doBegin(TransactionSynchronizationManager synchronizationMa
     					TransactionOptions.transactionOptions());
     			return holder.doOnNext(resourceHolder -> {
     				couchbaseTransactionObject.setResourceHolder(resourceHolder);
    -
     				if (logger.isDebugEnabled()) {
     					logger.debug(
     							String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore())));
     				}
    -
     			}).doOnNext(resourceHolder -> {
    -
     				couchbaseTransactionObject.startTransaction();
    -
     				if (logger.isDebugEnabled()) {
     					logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getCore())));
     				}
    -
     			})//
     					.onErrorMap(ex -> new TransactionSystemException(
     							String.format("Could not start Couchbase transaction for session %s.",
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveTransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveTransactionsWrapper.java
    new file mode 100644
    index 000000000..b4aa21ce7
    --- /dev/null
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveTransactionsWrapper.java
    @@ -0,0 +1,71 @@
    +package org.springframework.data.couchbase.transaction;
    +
    +import reactor.core.publisher.Mono;
    +
    +import java.util.function.Function;
    +
    +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory;
    +import org.springframework.transaction.ReactiveTransaction;
    +import org.springframework.transaction.TransactionDefinition;
    +import org.springframework.transaction.reactive.TransactionContextManager;
    +import org.springframework.transaction.reactive.TransactionSynchronizationManager;
    +
    +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor;
    +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext;
    +import com.couchbase.client.java.transactions.TransactionResult;
    +import com.couchbase.client.java.transactions.config.TransactionOptions;
    +
    +// todo gp needed now Transactions has gone?
    +public class ReactiveTransactionsWrapper /* wraps ReactiveTransactions */ {
    +	ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory;
    +
    +	public ReactiveTransactionsWrapper(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) {
    +		this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory;
    +	}
    +
    +	/**
    +	 * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default
    +	 * PerTransactionConfig.
    +	 */
    +
    +	public Mono run(Function> transactionLogic) {
    +		return run(transactionLogic, null);
    +	}
    +
    +	public Mono run(Function> transactionLogic,
    +			TransactionOptions perConfig) {
    +		// todo gp this is duplicating a lot of logic from the core loop, and is hopefully not needed.
    +		// todo ^^^ I think I removed all the duplicate logic.
    +		Function> newTransactionLogic = (ctx) -> {
    +			ReactiveCouchbaseResourceHolder resourceHolder = reactiveCouchbaseClientFactory.getTransactionResources(
    +					TransactionOptions.transactionOptions(), AttemptContextReactiveAccessor.getCore(ctx));
    +			Mono sync = TransactionContextManager.currentContext()
    +					.map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> {
    +						synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getBlockingCluster(), resourceHolder);
    +						prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition());
    +						return transactionLogic.apply(ctx) // <---- execute the transaction
    +								.thenReturn(ctx).then(Mono.just(synchronizationManager));
    +					});
    +			return sync.contextWrite(TransactionContextManager.getOrCreateContext())
    +					.contextWrite(TransactionContextManager.getOrCreateContextHolder());
    +		};
    +
    +		return reactiveCouchbaseClientFactory.getBlockingCluster().reactive().transactions().run(newTransactionLogic,
    +				perConfig);
    +
    +	}
    +
    +	private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager,
    +			ReactiveTransaction status, TransactionDefinition definition) {
    +		// if (status.isNewTransaction()) {
    +		synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/);
    +		synchronizationManager.setCurrentTransactionIsolationLevel(
    +				definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel()
    +						: null);
    +		synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
    +		synchronizationManager.setCurrentTransactionName(definition.getName());
    +		synchronizationManager.initSynchronization();
    +		// }
    +	}
    +
    +}
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java
    index e800f3e25..68243b731 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java
    @@ -1,157 +1,106 @@
     package org.springframework.data.couchbase.transaction;
     
    -import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
    +import static org.springframework.data.couchbase.transaction.CouchbaseTransactionManager.debugString;
    +import static org.springframework.data.couchbase.transaction.CouchbaseTransactionManager.newResourceHolder;
    +
    +import reactor.util.annotation.Nullable;
    +
    +import java.util.function.Consumer;
    +
    +import org.springframework.data.couchbase.CouchbaseClientFactory;
    +import org.springframework.transaction.support.TransactionSynchronizationManager;
    +
    +import com.couchbase.client.core.error.transaction.internal.CoreTransactionFailedException;
    +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
    +import com.couchbase.client.core.transaction.CoreTransactionResult;
    +import com.couchbase.client.core.transaction.log.CoreTransactionLogger;
     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.Transactions;
     import com.couchbase.client.java.transactions.config.TransactionOptions;
    -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory;
    -import org.springframework.transaction.ReactiveTransaction;
    -import org.springframework.transaction.TransactionDefinition;
    -import org.springframework.transaction.reactive.TransactionContextManager;
    -import org.springframework.transaction.reactive.TransactionSynchronizationManager;
    -import reactor.core.publisher.Mono;
    -
    -import java.time.Duration;
    -import java.time.temporal.ChronoUnit;
    -import java.util.function.Function;
    +import com.couchbase.client.java.transactions.error.TransactionFailedException;
     
     // todo gp needed now Transactions has gone?
    -public class TransactionsWrapper {
    -	ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory;
    +public class TransactionsWrapper /* wraps Transactions */ {
    +	CouchbaseClientFactory couchbaseClientFactory;
     
    -	public TransactionsWrapper(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) {
    -		this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory;
    +	public TransactionsWrapper(CouchbaseClientFactory couchbaseClientFactory) {
    +		this.couchbaseClientFactory = couchbaseClientFactory;
     	}
     
    +
     	/**
    -	 * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default
    -	 * PerTransactionConfig.
    +	 * Runs supplied transactional logic until success or failure.
    +	 * 

    + * The supplied transactional logic will be run if necessary multiple times, until either: + *

      + *
    • The transaction successfully commits
    • + *
    • The transactional logic requests an explicit rollback
    • + *
    • The transaction timesout.
    • + *
    • An exception is thrown, either inside the transaction library or by the supplied transaction logic, that cannot + * be handled. + *
    + *

    + * The transaction logic {@link Consumer} is provided an {@link TransactionAttemptContext}, which contains methods + * allowing it to read, mutate, insert and delete documents, as well as commit or rollback the transaction. + *

    + * If the transaction logic performs a commit or rollback it must be the last operation performed. Else a + * {@link com.couchbase.client.java.transactions.error.TransactionFailedException} will be thrown. Similarly, there + * cannot be a commit followed by a rollback, or vice versa - this will also raise a + * {@link CoreTransactionFailedException}. + *

    + * If the transaction logic does not perform an explicit commit or rollback, then a commit will be performed anyway. + * + * @param transactionLogic the application's transaction logic + * @param options the configuration to use for this transaction + * @return there is no need to check the returned {@link CoreTransactionResult}, as success is implied by the lack of + * a thrown exception. It contains information useful only for debugging and logging. + * @throws TransactionFailedException or a derived exception if the transaction fails to commit for any reason, + * possibly after multiple retries. The exception contains further details of the error */ - public Mono reactive(Function> transactionLogic) { - // TODO long duration for debugger - Duration duration = Duration.ofMinutes(20); - System.err.println("tx duration of " + duration); - return run(transactionLogic, TransactionOptions.transactionOptions().timeout(duration)); - } - public Mono run(Function> transactionLogic) { - return run(transactionLogic,null); - } - public Mono run(Function> transactionLogic, - TransactionOptions perConfig) { - // todo gp this is duplicating a lot of logic from the core loop, and is hopefully not needed.. - // todo mr it binds to with the TransactionSynchronizationManager - which is necessary. - Mono txResult = reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions().run((ctx) -> { - ReactiveCouchbaseResourceHolder resourceHolder = reactiveCouchbaseClientFactory - .getTransactionResources(TransactionOptions.transactionOptions(), AttemptContextReactiveAccessor.getCore(ctx)); - - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - Mono result = transactionLogic.apply(ctx); - result - .onErrorResume(err -> { - AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.toString(), "caught exception '%s' in async, rethrowing", err); - //logElidedStacktrace(ctx, err); - - return Mono.error(new TransactionOperationFailedException(true, true, err, null)); - }) - .thenReturn(ctx); - return result.then(Mono.just(synchronizationManager)); - }); - - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()); - }); - return txResult; - /* - TransactionsConfig config = TransactionsConfig.create().build(); - - ClusterEnvironment env = ClusterEnvironment.builder().build(); - return Mono.defer(() -> { - MergedTransactionsConfig merged = new MergedTransactionsConfig(config, Optional.of(perConfig)); - - TransactionContext overall = - new TransactionContext(env.requestTracer(), - env.eventBus(), - UUID.randomUUID().toString(), - now(), - Duration.ZERO, - merged); - AtomicReference startTime = new AtomicReference<>(0L); - - Mono ob = Mono.fromCallable(() -> { - String txnId = UUID.randomUUID().toString(); - //overall.LOGGER.info(configDebug(config, perConfig)); - return reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions().createAttemptContext(overall, merged, txnId); - }).flatMap(ctx -> { - - AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", - overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); - - // begin spring-data-couchbase transaction 1/2 * - ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory - .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, - reactiveCouchbaseClientFactory); - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - // end spring-data-couchbase transaction 1/2 - Mono result = transactionLogic.apply(ctx); - result - .onErrorResume(err -> { - AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); - logElidedStacktrace(ctx, err); - - return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); - }) - .thenReturn(ctx); - return result.then(Mono.just(synchronizationManager)); - }); - // begin spring-data-couchbase transaction 2/2 - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(Mono.just(ctx)); - // end spring-data-couchbase transaction 2/2 - }).doOnSubscribe(v -> startTime.set(System.nanoTime())) - .doOnNext(v -> AttemptContextReactiveAccessor.getLogger(v).trace(v.attemptId(), "finished attempt %d in %sms", - overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); - - return transactions.reactive().executeTransaction(merged, overall, ob) - .doOnNext(v -> overall.span().finish()) - .doOnError(err -> overall.span().failWith(err)); - }); - - */ - } + public TransactionResult run(Consumer transactionLogic, + @Nullable TransactionOptions options) { + Consumer newTransactionLogic = (ctx) -> { + try { + CoreTransactionLogger logger = AttemptContextReactiveAccessor.getLogger(ctx); + CoreTransactionAttemptContext atr = AttemptContextReactiveAccessor.getCore(ctx); - // private void logElidedStacktrace(ReactiveTransactionAttemptContext ctx, Throwable err) { - // transactions.reactive().logElidedStacktrace(ctx, err); - // } - // - // private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { - // return transactions.reactive().configDebug(config, perConfig); - // } - // - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + // from CouchbaseTransactionManager + ReactiveCouchbaseResourceHolder resourceHolder = newResourceHolder(couchbaseClientFactory, + /*definition*/ new CouchbaseTransactionDefinition(), TransactionOptions.transactionOptions(), atr); + // couchbaseTransactionObject.setResourceHolder(resourceHolder); + + logger + .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); + + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getCore()))); + + TransactionSynchronizationManager.setActualTransactionActive(true); + resourceHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.unbindResourceIfPossible(couchbaseClientFactory.getCluster()); + logger.debug("CouchbaseTransactionManager: " + this); + logger.debug("bindResource: " + couchbaseClientFactory.getCluster() + " value: " + resourceHolder); + TransactionSynchronizationManager.bindResource(couchbaseClientFactory.getCluster(), resourceHolder); + + transactionLogic.accept(ctx); + } finally { + TransactionSynchronizationManager.unbindResource(couchbaseClientFactory.getCluster()); + } + }; + + return AttemptContextReactiveAccessor.run(couchbaseClientFactory.getCluster().transactions(), newTransactionLogic, + options == null ? null : options.build()); } - private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, - ReactiveTransaction status, TransactionDefinition definition) { - - // if (status.isNewSynchronization()) { - synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); - synchronizationManager.setCurrentTransactionIsolationLevel( - definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() - : null); - synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); - synchronizationManager.setCurrentTransactionName(definition.getName()); - synchronizationManager.initSynchronization(); - // } + /** + * Runs supplied transactional logic until success or failure. A convenience overload for {@link Transactions#run} + * that provides a default PerTransactionConfig + */ + + public TransactionResult run(Consumer transactionLogic) { + return run(transactionLogic, null); } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index 9919adcdb..8a345742b 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -24,19 +24,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; -import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; -import com.couchbase.client.java.transactions.config.TransactionOptions; import lombok.Data; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; import java.time.Duration; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -53,16 +50,15 @@ import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; -import org.springframework.data.couchbase.transaction.ClientSessionOptions; import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; -import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; -import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.ReactiveTransactionsWrapper; import org.springframework.data.couchbase.transaction.TransactionsWrapper; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -71,23 +67,17 @@ import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.ReactiveTransaction; -import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.reactive.TransactionContextManager; -import org.springframework.transaction.reactive.TransactionSynchronizationManager; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.transaction.support.DefaultTransactionDefinition; import com.couchbase.client.core.error.DocumentNotFoundException; -import com.couchbase.client.core.error.transaction.RetryTransactionException; import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.transactions.TransactionResult; -import com.couchbase.client.java.transactions.Transactions; import com.couchbase.client.java.transactions.error.TransactionFailedException; /** @@ -115,38 +105,37 @@ public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationT // if these are changed from default, then beforeEach needs to clean up separately String sName = "_default"; String cName = "_default"; - - // static GenericApplicationContext context; + private TransactionalOperator transactionalOperator; @BeforeAll public static void beforeAll() { callSuperBeforeAll(new Object() {}); - // context = new AnnotationConfigApplicationContext(Config.class, PersonService.class); } @AfterAll public static void afterAll() { callSuperAfterAll(new Object() {}); - // if (context != null) { - // context.close(); - // } } @BeforeEach public void beforeEachTest() { - // personService = context.getBean(PersonService.class); // getting it via autowired results in no @Transactional - // Skip this as we just one to track TransactionContext - operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); // doesn't work??? - operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); - List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); - List e = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List rp0 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List rp1 = operations.removeByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + List rp2 = operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List rp3 = operations.removeByQuery(EventLog.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + + List p0 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List p1 = operations.findByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + List e0 = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List e1 = operations.findByQuery(EventLog.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); Person walterWhite = new Person(1, "Walter", "White"); - try { - couchbaseClientFactory.getBucket().defaultCollection().remove(walterWhite.getId().toString()); - } catch (Exception ex) { - // System.err.println(ex); - } + remove(cbTmpl, sName, cName, walterWhite.getId().toString()); + transactionalOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager); } @Test @@ -243,6 +232,12 @@ public void emitMultipleElementsDuringTransaction() { public void errorAfterTxShouldNotAffectPreviousStep() { Person p = personService.savePerson(new Person(null, "Walter", "White")); // todo gp user shouldn't be getting exposed to TransactionOperationFailedException + // todo mr + /* + TransactionOperationFailedException {cause:com.couchbase.client.core.error.DocumentExistsException, retry:false, autoRollback:true, raise:TRANSACTION_FAILED} + at com.couchbase.client.core.error.transaction.TransactionOperationFailedException$Builder.build(TransactionOperationFailedException.java:136) + at com.couchbase.client.core.transaction.CoreTransactionAttemptContext.lambda$handleDocExistsDuringStagedInsert$116(CoreTransactionAttemptContext.java:1801) + */ assertThrows(TransactionOperationFailedException.class, () -> personService.savePerson(p)); Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); assertEquals(1, count, "should have saved and found 1"); @@ -256,48 +251,20 @@ public void errorAfterTxShouldNotAffectPreviousStep() { public void replacePersonCBTransactionsRxTmpl() { Person person = new Person(1, "Walter", "White"); cbTmpl.insertById(Person.class).one(person); - Mono result = this.couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // - .flatMap((pp) -> rxCBTmpl.replaceById(Person.class).one(pp)) // - .then(Mono.just(synchronizationManager)); // tx - }); - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); - }); - + Mono result = rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap((pp) -> rxCBTmpl.replaceById(Person.class).one(pp)) // + .as(transactionalOperator::transactional); result.block(); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); - assertEquals(person, pFound, "should have found expected"); + assertEquals(person, pFound, "should have found expected " + person); } @Test public void insertPersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); - try { - rxCBTmpl.removeById(Person.class).one(person.getId().toString()); - } catch (DocumentNotFoundException dnfe) {} - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - // execute the transaction (insertById, SimulateFailure), insertById() will fetch the ctx from the context - return rxCBTmpl.insertById(Person.class).one(person).then(Mono.error(new SimulateFailureException())); // tx - }); - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); - - }); - assertThrowsCause(TransactionFailedException.class, SimulateFailureException.class, (ignore) -> { - result.block(); - return null; - }); + Mono result = rxCBTmpl.insertById(Person.class).one(person) // + .flatMap(p -> throwSimulatedFailure(p)).as(transactionalOperator::transactional); // tx + assertThrows(SimulateFailureException.class, result::block); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertNull(pFound, "insert should have been rolled back"); } @@ -305,93 +272,63 @@ public void insertPersonCBTransactionsRxTmplRollback() { @Test public void insertTwicePersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); - // - Transactions transactions = this.couchbaseClientFactory.getCluster().transactions(); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { - - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - return rxCBTmpl.insertById(Person.class).one(person) // - .flatMap((ppp) -> rxCBTmpl.insertById(Person.class).one(ppp)) // - .then(Mono.just(synchronizationManager)); // tx - }); - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); - }); - assertThrowsCause(TransactionFailedException.class, DuplicateKeyException.class, (ignore) -> { - result.block(); - return null; - }); + Mono result = rxCBTmpl.insertById(Person.class).one(person) // + .flatMap((ppp) -> rxCBTmpl.insertById(Person.class).one(ppp)) // + .as(transactionalOperator::transactional); + assertThrows(DuplicateKeyException.class, result::block); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertNull(pFound, "insert should have been rolled back"); } - /** - * This test has the bare minimum for reactive transactions. Create the ClientSession that holds the ctx and put it in - * a resourceHolder and binds it to the currentContext. The retries are handled by couchbase-transactions - which - * creates a new ctx and re-runs the lambda. This is essentially what TransactionWrapper does. - */ @Test - public void replaceWithCasConflictResolvedViaRetry() { + public void wrapperReplaceWithCasConflictResolvedViaRetry() { Person person = new Person(1, "Walter", "White"); Person switchedPerson = new Person(1, "Dave", "Reynolds"); - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); - t.start(); - cbTmpl.insertById(Person.class).one(person); - AtomicInteger tryCount = new AtomicInteger(0); - Mono result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); - Mono sync = TransactionContextManager.currentContext() - .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // - .flatMap((ppp) -> { - tryCount.getAndIncrement(); - System.err.println("===== ATTEMPT : " + tryCount.get() + " ====="); - return Mono.just(ppp); - })// - .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)) // - .then(Mono.just(synchronizationManager)); // tx - }); - return sync.contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); - }); - - result.block(); + cbTmpl.insertById(Person.class).one(person); - stop.set(true); - Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); - assertTrue(tryCount.get() > 1, "should have been more than one try "); + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); + t.start(); + tryCount.set(0); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(couchbaseClientFactory); + TransactionResult txResult = transactionsWrapper.run(ctx -> { + System.err.println("try: " + tryCount.incrementAndGet()); + Person ppp = cbTmpl.findById(Person.class).one(person.getId().toString()); + Person pppp = cbTmpl.replaceById(Person.class).one(ppp); + }); + System.out.println("txResult: " + txResult); + t.setStopFlag(); + if (tryCount.get() > 1) { + break; + } + } + Person pFound = cbTmpl.findById(Person.class).one(person.getId().toString()); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); } @Test - public void wrapperReplaceWithCasConflictResolvedViaRetry() { + public void wrapperReplaceWithCasConflictResolvedViaRetryReactive() { Person person = new Person(1, "Walter", "White"); Person switchedPerson = new Person(1, "Dave", "Reynolds"); AtomicInteger tryCount = new AtomicInteger(0); cbTmpl.insertById(Person.class).one(person); - for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); t.start(); tryCount.set(0); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.reactive(ctx -> { + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { System.err.println("try: " + tryCount.incrementAndGet()); return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)).then(); }); TransactionResult txResult = result.block(); - stop.set(true); System.out.println("txResult: " + txResult); + t.setStopFlag(); if (tryCount.get() > 1) { break; } @@ -403,10 +340,7 @@ public void wrapperReplaceWithCasConflictResolvedViaRetry() { /** * This does process retries - by CallbackTransactionManager.execute() -> transactions.run() -> executeTransaction() - * -> retryWhen. The CallbackTransactionManager only finds the resources in the Thread - it doesn't find it in the - * context. It might be nice to use the context for both - but I'm not sure if that is possible - mostly due to - * ExecutableFindById.one() calling reactive.one().block() instead of returning a publisher which could have - * .contextWrite() called on it. + * -> retryWhen. */ @Test public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { @@ -415,14 +349,12 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { AtomicInteger tryCount = new AtomicInteger(0); cbTmpl.insertById(Person.class).one(person); - for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); t.start(); tryCount.set(0); - Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount); - stop.set(true); + t.setStopFlag(); if (tryCount.get() > 1) { break; } @@ -430,7 +362,6 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); - } /** @@ -443,22 +374,22 @@ public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { Person switchedPerson = new Person(1, "Dave", "Reynolds"); cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); t.start(); tryCount.set(0); - for (;;) { - Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount) - .onErrorResume(RetryTransactionException.class, (thrown) -> Mono.empty()).block(); - if (res != null) - break; - } - stop.set(true); + // TODO mr - Graham says not to do delegate retries to user. He's a party pooper. + Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount) + .retryWhen(Retry.backoff(10, Duration.ofMillis(50)) + .filter(throwable -> throwable instanceof TransactionOperationFailedException) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + throw new RuntimeException("Transaction failed after max retries"); + })) + .block(); + t.setStopFlag(); if (tryCount.get() > 1) { break; } - } Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); @@ -476,13 +407,12 @@ public void replaceWithCasConflictResolvedViaRetryAnnotated() { cbTmpl.insertById(Person.class).one(person); AtomicInteger tryCount = new AtomicInteger(0); - for (int i = 0; i < 10; i++) { // the transaction sometimes succeeds on the first try - AtomicBoolean stop = new AtomicBoolean(false); - Thread t = new ReplaceLoopThread(stop, switchedPerson); + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); t.start(); tryCount.set(0); Person p = personService.declarativeFindReplacePerson(person, tryCount); - stop.set(true); + t.setStopFlag(); if (tryCount.get() > 1) { break; } @@ -493,68 +423,19 @@ public void replaceWithCasConflictResolvedViaRetryAnnotated() { assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); } - private class ReplaceLoopThread extends Thread { - AtomicBoolean stop; - Person person; - int maxIterations = 100; - - public ReplaceLoopThread(AtomicBoolean stop, Person person, int... iterations) { - this.stop = stop; - this.person = person; - if (iterations != null && iterations.length == 1) { - this.maxIterations = iterations[0]; - } - } - - public void run() { - for (int i = 0; i < maxIterations && !stop.get(); i++) { - sleepMs(10); - try { - // note that this does not go through spring-data, therefore it does not have the @Field , @Version etc. - // annotations processed so we just check getFirstname().equals() - // switchedPerson has version=0, so it doesn't check CAS - couchbaseClientFactory.getBucket().defaultCollection().replace(person.getId().toString(), person); - System.out.println("********** replace thread: " + i + " success"); - } catch (Exception e) { - System.out.println("********** replace thread: " + i + " " + e.getClass().getName()); - } - } - - } - } - @Test public void replacePersonCBTransactionsRxTmplRollback() { Person person = new Person(1, "Walter", "White"); String newName = "Walt"; rxCBTmpl.insertById(Person.class).one(person).block(); - Mono result = reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions() - .run(ctx -> { - // can we take the ReactiveTransactionAttemptContext ctx and save it in the context? - // how we get from non-reactive to reactive? - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); - - assertEquals(reactiveCouchbaseClientFactory.getCluster().block(), couchbaseClientFactory.getCluster()); - // I think this needs to happen within the couchbaseClientFactory.getCluster().reactive().transactions().run() - // call - or equivalent. - // this currentContext() call is going to create a new ctx, and store the acr. Will it get used in - // syncFlatMap() - // below? Should the ctx be created in the above call to - // couchbaseClientFactory.getCluster().reactive().transactions().run()? - // How does this work in savePerson etc? - // is there means for just getting the currentContext() without creating it? - return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new) - .flatMap(synchronizationManager -> { - // is this the correct sync Manager?? - synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), - resourceHolder); - prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); - return rxCBTmpl.findById(Person.class).one(person.getId().toString()); - }) // need to get the TSM context in the one() calls. - .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName(newName))).then(Mono.empty()); - }).contextWrite(TransactionContextManager.getOrCreateContext()) - .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + // doesn't TransactionWrapper do the same thing? + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // + return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName(newName))).then(Mono.empty()); + }); result.block(); Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); @@ -565,11 +446,12 @@ public void replacePersonCBTransactionsRxTmplRollback() { @Test public void deletePersonCBTransactionsRxTmpl() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> { // get the ctx + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person).then(); }); result.block(); @@ -580,11 +462,12 @@ public void deletePersonCBTransactionsRxTmpl() { @Test public void deletePersonCBTransactionsRxTmplFail() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); cbTmpl.insertById(Person.class).inCollection(cName).one(person); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> { // get the ctx + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person) .then(rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person)).then(); }); @@ -593,16 +476,15 @@ public void deletePersonCBTransactionsRxTmplFail() { assertEquals(pFound, person, "Should have found " + person); } - // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// - @Test public void deletePersonCBTransactionsRxRepo() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); rxRepo.withCollection(cName).save(person).block(); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> { // get the ctx + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx return rxRepo.withCollection(cName).delete(person).then(); }); result.block(); @@ -613,92 +495,86 @@ public void deletePersonCBTransactionsRxRepo() { @Test public void deletePersonCBTransactionsRxRepoFail() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); rxRepo.withCollection(cName).save(person).block(); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> { // get the ctx - return rxRepo.withCollection(cName).delete(person).then(rxRepo.withCollection(cName).delete(person)).then(); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx + return rxRepo.withCollection(cName).findById(person.getId().toString()) + .flatMap(pp -> rxRepo.withCollection(cName).delete(pp).then(rxRepo.withCollection(cName).delete(pp))).then(); }); assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class); Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertEquals(pFound, person, "Should have found " + person); + assertEquals(pFound, person, "Should have found " + person + " instead of " + pFound); } @Test public void findPersonCBTransactions() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); List docs = new LinkedList<>(); Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class).inScope(sName) - .inCollection(cName).matching(q).withConsistency(REQUEST_PLUS).one().doOnSuccess(doc -> docs.add(doc))); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class) + .inScope(sName).inCollection(cName).matching(q).withConsistency(REQUEST_PLUS).one().doOnSuccess(doc -> { + System.err.println("doc: " + doc); + docs.add(doc); + })); result.block(); assertFalse(docs.isEmpty(), "Should have found " + person); for (Object o : docs) { - assertEquals(o, person, "Should have found " + person); + assertEquals(o, person, "Should have found " + person + " instead of " + o); } } @Test public void insertPersonRbCBTransactions() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.insertById(Person.class).inScope(sName) - .inCollection(cName).one(person). flatMap(it -> Mono.error(new SimulateFailureException()))); - try { - result.block(); - } catch (TransactionFailedException e) { - if (e.getCause() instanceof SimulateFailureException) { - Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); - assertNull(pFound, "Should not have found " + pFound); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper + .run(ctx -> rxCBTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person) + . flatMap(it -> Mono.error(new SimulateFailureException()))); + + assertThrowsWithCause(() -> result.block(), TransactionFailedException.class, SimulateFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); } @Test public void replacePersonRbCBTransactions() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.findById(Person.class).inScope(sName) - .inCollection(cName).one(person.getId().toString()).flatMap(pFound -> rxCBTmpl.replaceById(Person.class) - .inScope(sName).inCollection(cName).one(pFound.withFirstName("Walt"))) + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> // + rxCBTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.getId().toString()) // + .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inScope(sName).inCollection(cName) + .one(pFound.withFirstName("Walt"))) . flatMap(it -> Mono.error(new SimulateFailureException()))); - try { - result.block(); - } catch (TransactionFailedException e) { - if (e.getCause() instanceof SimulateFailureException) { - Person pFound = cbTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.getId().toString()); - assertEquals(person, pFound, "Should have found " + person); - return; - } else { - e.printStackTrace(); - } - } - throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + assertThrowsWithCause(() -> result.block(), TransactionFailedException.class, SimulateFailureException.class); + + Person pFound = cbTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person + " instead of " + pFound); } @Test public void findPersonSpringTransactions() { Person person = new Person(1, "Walter", "White"); - remove(cbTmpl, cName, person.getId().toString()); + remove(cbTmpl, sName, cName, person.getId().toString()); cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); - // sleepMs(1000); List docs = new LinkedList<>(); Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); - TransactionsWrapper transactionsWrapper = new TransactionsWrapper(reactiveCouchbaseClientFactory); - Mono result = transactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class).inScope(sName) - .inCollection(cName).matching(q).one().doOnSuccess(r -> docs.add(r))); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class) + .inScope(sName).inCollection(cName).matching(q).one().doOnSuccess(r -> docs.add(r))); result.block(); assertFalse(docs.isEmpty(), "Should have found " + person); for (Object o : docs) { @@ -706,6 +582,39 @@ public void findPersonSpringTransactions() { } } + private class ReplaceLoopThread extends Thread { + AtomicBoolean stop = new AtomicBoolean(false); + Person person; + int maxIterations = 100; + + public ReplaceLoopThread(Person person, int... iterations) { + this.person = person; + if (iterations != null && iterations.length == 1) { + this.maxIterations = iterations[0]; + } + } + + public void run() { + for (int i = 0; i < maxIterations && !stop.get(); i++) { + sleepMs(10); + try { + // note that this does not go through spring-data, therefore it does not have the @Field , @Version etc. + // annotations processed so we just check getFirstname().equals() + // switchedPerson has version=0, so it doesn't check CAS + couchbaseClientFactory.getBucket().defaultCollection().replace(person.getId().toString(), person); + System.out.println("********** replace thread: " + i + " success"); + } catch (Exception e) { + System.out.println("********** replace thread: " + i + " " + e.getClass().getName()); + } + } + + } + + public void setStopFlag() { + stop.set(true); + } + } + void remove(Collection col, String id) { remove(col.reactive(), id); } @@ -718,64 +627,33 @@ void remove(ReactiveCollection col, String id) { } } - void remove(CouchbaseTemplate template, String collection, String id) { - remove(template.reactive(), collection, id); + void remove(CouchbaseTemplate template, String scope, String collection, String id) { + remove(template.reactive(), scope, collection, id); } - void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + void remove(ReactiveCouchbaseTemplate template, String scope, String collection, String id) { try { - template.removeById(Person.class).inCollection(collection).one(id).block(); + template.removeById(Person.class).inScope(scope).inCollection(collection).one(id).block(); System.out.println("removed " + id); + List ps = template.findByQuery(Person.class).inScope(scope).inCollection(collection) + .withConsistency(REQUEST_PLUS).all().collectList().block(); } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { System.out.println(id + " : " + "DocumentNotFound when deleting"); } } - private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, - ReactiveTransaction status, TransactionDefinition definition) { - - // if (status.isNewSynchronization()) { - synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); - synchronizationManager.setCurrentTransactionIsolationLevel( - definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() - : null); - synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); - synchronizationManager.setCurrentTransactionName(definition.getName()); - synchronizationManager.initSynchronization(); - // } - } - - void assertThrowsCause(Class exceptionClass, Class causeClass, Function function) { - try { - function.apply(null); - } catch (Throwable tfe) { - System.err.println("Exception: " + tfe + " causedBy: " + tfe.getCause()); - if (tfe.getClass().isAssignableFrom(exceptionClass)) { - if (tfe.getCause() != null && tfe.getCause().getClass().isAssignableFrom(causeClass)) { - System.err.println("thrown exception was: " + tfe + " cause: " + tfe.getCause()); - return; - } - } - throw new RuntimeException("expected " + exceptionClass + " with cause " + causeClass + " but got " + tfe); - } - throw new RuntimeException("expected " + exceptionClass + " with cause " + causeClass + " nothing was thrown"); + private Mono throwSimulatedFailure(T p) { + throw new SimulateFailureException(); } @Data - // @AllArgsConstructor static class EventLog { - public EventLog() {} public EventLog(ObjectId oid, String action) { this.id = oid.toString(); this.action = action; } - public EventLog(String id, String action) { - this.id = id; - this.action = action; - } - String id; String action; @@ -830,9 +708,7 @@ public String toString() { @Service @Component @EnableTransactionManagement - static - // @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) - class PersonService { + static class PersonService { final CouchbaseOperations personOperations; final CouchbaseSimpleCallbackTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; @@ -925,14 +801,6 @@ public Person declarativeSavePerson(Person person) { return personOperations.insertById(Person.class).one(person); } - public Person savePersonBlocking(Person person) { - if (1 == 1) - throw new RuntimeException("not implemented"); - assertInAnnotationTransaction(true); - return personOperations.insertById(Person.class).one(person); - - } - @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) public Person declarativeSavePersonErrors(Person person) { assertInAnnotationTransaction(true); @@ -941,8 +809,6 @@ public Person declarativeSavePersonErrors(Person person) { return p; } - @Autowired CouchbaseSimpleCallbackTransactionManager callbackTm; - /** * to execute while ThreadReplaceloop() is running should force a retry * @@ -953,11 +819,6 @@ public Person declarativeSavePersonErrors(Person person) { public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); - // System.err.println("declarativeFindReplacePersonCallback cluster : " - // + callbackTm.template().getCouchbaseClientFactory().getCluster().block()); - // System.err.println("declarativeFindReplacePersonCallback resourceHolder : " - // + org.springframework.transaction.support.TransactionSynchronizationManager - // .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); Person p = personOperations.findById(Person.class).one(person.getId().toString()); return personOperations.replaceById(Person.class).one(p.withFirstName(person.getFirstname())); } @@ -973,12 +834,6 @@ public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { assertInAnnotationTransaction(true); System.err.println("declarativeFindReplacePersonReactive try: " + tryCount.incrementAndGet()); - /* NoTransactionInContextException - TransactionSynchronizationManager.forCurrentTransaction().flatMap( sm -> { - System.err.println("declarativeFindReplacePersonReactive reactive resourceHolder : "+sm.getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); - return Mono.just(sm); - }).block(); - */ return personOperationsRx.findById(Person.class).one(person.getId().toString()) .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(person.getFirstname()))); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java index 69b5536e1..dacfab844 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -369,7 +369,6 @@ public void insertReplacePersonsCBTransactionsRxTmpl() { public Mono savePerson(Person person) { TransactionalOperator transactionalOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager); - return operations.save(person) // .flatMap(Mono::just) // .as(transactionalOperator::transactional); diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index 991fecefe..975170cca 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -209,9 +209,8 @@ protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, fin } if (!ready) { - createAndDeleteBucket();// need to do this because of https://issues.couchbase.com/browse/MB-50132 try { - Thread.sleep(50); + Thread.sleep(100); } catch (InterruptedException e) {} } } @@ -221,33 +220,6 @@ protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, fin } } - private static void createAndDeleteBucket() { - final OkHttpClient httpClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build(); - String hostPort = connectionString().replace("11210", "8091").replace("11207", "18091"); - String protocol = hostPort.equals("18091") ? "https" : "http"; - String bucketname = UUID.randomUUID().toString(); - try { - - Response postResponse = httpClient.newCall(new Request.Builder() - .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url(protocol+"://" + hostPort + "/pools/default/buckets/") - .post(new FormBody.Builder().add("name", bucketname).add("bucketType", "membase").add("ramQuotaMB", "100") - .add("replicaNumber", Integer.toString(0)).add("flushEnabled", "1").build()) - .build()).execute(); - - if (postResponse.code() != 202) { - throw new IOException("Could not create bucket: " + postResponse + ", Reason: " + postResponse.body().string()); - } - Response deleteResponse = httpClient.newCall(new Request.Builder() - .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url(protocol+"://" + hostPort + "/pools/default/buckets/" + bucketname).delete().build()).execute(); - System.out.println("deleteResponse: " + deleteResponse); - } catch (IOException ioe) { - ioe.printStackTrace(); - } - } - /** * Improve test stability by waiting for a given service to report itself ready. */ From f46bb53f5b484beb7a15abb916fd6a5dd3659918 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Thu, 26 May 2022 18:10:28 -0700 Subject: [PATCH 9/9] Programmatix datacouch 1145 transaction support (#1446) * Porting to SDK-integrated version of transactions The transactions logic exists in the Java SDK as of 3.3.0, with a slightly different API. This is the first effort at the port, which literally just compiles. It will not run as crucial code has been commented and todo-ed. There is work remaining to figure out how to complete the port, as some crucial parts (such as ctx.commit() and ctx.rollback()) have been intentionally removed. * Continuing work to get the ExtSDKIntegration port working Trying to transition to CallbackPreferring manager. * Added CouchbaseSimpleCallbackTransactionManager, the simplest possible implementation of CallbackPreferringTransactionManager, combined with a simpler approach to ThreadLocal storage in ReactiveInsertByIdSupport. Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now passing. * Adding WIP get-and-replace @Transactional support (Not yet working as CAS/version field in Person is not populated correctly.) * Transitioning to use CoreTransactionAttemptContext. Tests may fail. * Removing AttemptContextReactiveAccessor Don't think we need this, as we can pass around CoreTransactionAttemptContext instead, which gives access to a lot of internals. * Removing TransactionsReactive Would prefer not to C&P a huge class out of the transaction internals, and don't think we need it. * Removing some files not currently used To reduce & simplify the amount of code to look at. Some don't seem to be used in any branch, some just aren't used in this branch. * Removing CouchbaseTransactionInterceptor As per offline discussion, CallbackPreferringPlatformTransactionManager is perhaps the optimal solution. * Copying @Transactional tests out into separate class * Tidyup * Tidyup test names * Verify GenericSupport is on same thread before and after transactional operation * Refactoring CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage management * Using latest java-client * ReactiveReplaceByIdSupport - Fixing use of CAS now have CoreTransactionAttemptContext. Removing unused code. * ReactiveInsertByIdSupport - fixing use of reactive vs non-reactive, and CAS * Merging upstream * Remove incorrect thread check (.doOnNext could execute on a different thread) * Completing merge from upstream * Removing unused classes * Give GenericSupport a better name * Reject at runtime options that aren't supported in a transaction * Fixing some small todos, partly by removing unused coe * Fix runtime option checks * Simplifying CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage Standardising on ReactiveCouchbaseResourceHolder rather than holding CoreTransactionAttemptContext too * Removing version from CouchbaseDocument Can't recall why I added this, and tests pass without it * Improving CouchbaseTransactionalIntegrationTests and adding more tests * Reject operations that aren't allowed in a transaction (upsertById etc.) * Improve handling of CAS mismatch By calling CoreTransactionAttemptContext.operationFailed, it ensures that internal state is set. So even if the user catches the exception, the transaction still behaves as it should. * Removing a now-redundant non-transactional check on upsertById I missed this when adding TransactionalSupport.verifyNotInTransaction here. * Support @Transactional options timeout and isolation level Co-authored-by: Graham Pople --- .../com/couchbase/client/java/Cluster.java | 8 +- .../client/java/ClusterInterface.java | 2 +- .../AttemptContextReactiveAccessor.java | 40 +- .../demo/CouchbaseTransactionalTemplate.java | 3 +- .../demo/SpringTransactionGetResult.java | 6 +- .../couchbase/CouchbaseClientFactory.java | 2 +- .../ReactiveCouchbaseClientFactory.java | 4 +- .../SimpleCouchbaseClientFactory.java | 14 +- .../SimpleReactiveCouchbaseClientFactory.java | 14 +- .../AbstractCouchbaseConfiguration.java | 19 +- .../core/AbstractTemplateSupport.java | 4 +- .../couchbase/core/CouchbaseOperations.java | 2 +- .../couchbase/core/CouchbaseTemplate.java | 14 +- .../core/CouchbaseTemplateSupport.java | 10 +- .../ExecutableFindByIdOperationSupport.java | 2 +- .../core/ExecutableFindByQueryOperation.java | 2 +- ...ExecutableFindByQueryOperationSupport.java | 6 +- .../ExecutableInsertByIdOperationSupport.java | 4 +- .../ExecutableRemoveByIdOperationSupport.java | 4 +- ...ecutableRemoveByQueryOperationSupport.java | 4 +- .../core/ExecutableReplaceByIdOperation.java | 1 + ...ExecutableReplaceByIdOperationSupport.java | 4 +- .../core/NonReactiveSupportWrapper.java | 8 +- .../core/ReactiveCouchbaseTemplate.java | 12 +- .../ReactiveCouchbaseTemplateSupport.java | 10 +- .../ReactiveExistsByIdOperationSupport.java | 4 +- ...activeFindByAnalyticsOperationSupport.java | 5 +- .../ReactiveFindByIdOperationSupport.java | 49 +- .../core/ReactiveFindByQueryOperation.java | 2 +- .../ReactiveFindByQueryOperationSupport.java | 74 +-- ...eFindFromReplicasByIdOperationSupport.java | 3 +- .../ReactiveInsertByIdOperationSupport.java | 48 +- .../ReactiveRemoveByIdOperationSupport.java | 39 +- .../core/ReactiveRemoveByQueryOperation.java | 4 +- ...ReactiveRemoveByQueryOperationSupport.java | 18 +- .../core/ReactiveReplaceByIdOperation.java | 2 +- .../ReactiveReplaceByIdOperationSupport.java | 69 +-- .../core/ReactiveTemplateSupport.java | 4 +- .../ReactiveUpsertByIdOperationSupport.java | 23 +- .../data/couchbase/core/TemplateSupport.java | 2 +- .../couchbase/core/TransactionalSupport.java | 95 ++++ .../couchbase/core/support/PseudoArgs.java | 4 +- .../core/support/WithTransaction.java | 4 +- .../repository/DynamicProxyable.java | 2 +- .../support/CouchbaseRepositoryBase.java | 2 +- .../support/DynamicInvocationHandler.java | 2 +- .../SimpleReactiveCouchbaseRepository.java | 2 +- .../transaction/AbortCommitSubscriber.java | 67 --- .../transaction/ClientSessionImplx.java | 279 ---------- .../transaction/ClientSessionOptions.java | 114 ---- .../couchbase/transaction/ClientSessionx.java | 62 --- .../CouchbaseAttemptContextReactive.java | 103 ---- ...hbaseSimpleCallbackTransactionManager.java | 77 ++- .../CouchbaseTransactionManager.java | 32 +- ...hbaseTransactionalOperatorNonReactive.save | 197 ------- .../ReactiveCouchbaseClientUtils.java | 20 +- .../ReactiveCouchbaseResourceHolder.java | 1 - .../ReactiveCouchbaseTransactionManager.java | 31 +- .../SessionAwareMethodInterceptor.java | 12 +- .../transaction/TransactionsWrapper.java | 245 +++++---- .../internal/AsyncClientSession.save | 26 - .../internal/BaseClientSessionImpl.save | 170 ------ .../internal/ClientSessionPublisherImpl.save | 241 --------- .../internal/SingleResultCallback.java | 10 - .../data/couchbase/domain/Config.java | 6 +- .../domain/PersonWithoutVersion.java | 190 +++++++ ...chbaseRepositoryQueryIntegrationTests.java | 2 +- ...basePersonTransactionIntegrationTests.java | 20 +- ...onTransactionReactiveIntegrationTests.java | 44 +- ...uchbaseReactiveTransactionNativeTests.java | 4 +- ...seTemplateTransactionIntegrationTests.java | 2 +- ...ouchbaseTransactionalIntegrationTests.java | 497 ++++++++++++++++++ ...onAllowableOperationsIntegrationTests.java | 148 ++++++ ...eTransactionalOptionsIntegrationTests.java | 147 ++++++ ...lUnsettableParametersIntegrationTests.java | 193 +++++++ .../couchbase/util/JavaIntegrationTests.java | 24 +- .../data/couchbase/util/Util.java | 26 +- 77 files changed, 1864 insertions(+), 1782 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImplx.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/ClientSessionx.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.save delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save delete mode 100644 src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java diff --git a/src/main/java/com/couchbase/client/java/Cluster.java b/src/main/java/com/couchbase/client/java/Cluster.java index 0b7a889b2..23c588033 100644 --- a/src/main/java/com/couchbase/client/java/Cluster.java +++ b/src/main/java/com/couchbase/client/java/Cluster.java @@ -103,7 +103,7 @@ * The SDK will only work against Couchbase Server 5.0 and later, because RBAC (role-based access control) is a first * class concept since 3.0 and therefore required. */ -// todo gp is this required? +// todo gpx as per discussion with miker - if required, ClusterInterface will be added to the SDK instead public class Cluster implements ClusterInterface { /** @@ -256,9 +256,9 @@ public static Cluster connect(final String connectionString, final ClusterOption final ClusterOptions.Built opts = options.build(); final Supplier environmentSupplier = extractClusterEnvironment(connectionString, opts); return new Cluster( - environmentSupplier, - opts.authenticator(), - seedNodesFromConnectionString(connectionString, environmentSupplier.get()) + environmentSupplier, + opts.authenticator(), + seedNodesFromConnectionString(connectionString, environmentSupplier.get()) ); } diff --git a/src/main/java/com/couchbase/client/java/ClusterInterface.java b/src/main/java/com/couchbase/client/java/ClusterInterface.java index 341c6880d..872a6efdf 100644 --- a/src/main/java/com/couchbase/client/java/ClusterInterface.java +++ b/src/main/java/com/couchbase/client/java/ClusterInterface.java @@ -83,7 +83,7 @@ public interface ClusterInterface { //AnalyticsResult analyticsQuery(String statement); - // AnalyticsResult analyticsQuery(String statement, AnalyticsOptions options); + // AnalyticsResult analyticsQuery(String statement, AnalyticsOptions options); SearchResult searchQuery(String indexName, SearchQuery query); diff --git a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java index 15dfe35f5..f0ffd69bd 100644 --- a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java +++ b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java @@ -44,12 +44,6 @@ */ public class AttemptContextReactiveAccessor { - public static ReactiveTransactionAttemptContext getACR(TransactionAttemptContext attemptContext) { - // return attemptContext.ctx(); - // todo gp is this access needed. Could hold the raw CoreTransactionAttemptContext instead. - return null; - } - public static ReactiveTransactions reactive(Transactions transactions) { try { Field field = Transactions.class.getDeclaredField("reactive"); @@ -212,26 +206,18 @@ public static TransactionResult run(Transactions transactions, Consumer SpringTransactionGetResult findById(String id, Class domainType CoreTransactionAttemptContext ctx = getContext(); CoreTransactionGetResult getResult = ctx.get( makeCollectionIdentifier(template.getCouchbaseClientFactory().getDefaultCollection().async()) , id).block(); - // todo gp getResult.cas() is no longer exposed - required? - T t = template.support().decodeEntity(id, new String(getResult.contentAsBytes()), 0, domainType, + T t = template.support().decodeEntity(id, new String(getResult.contentAsBytes()), getResult.cas(), domainType, null, null, null); return new SpringTransactionGetResult<>(t, getResult); } catch (Exception e) { diff --git a/src/main/java/com/example/demo/SpringTransactionGetResult.java b/src/main/java/com/example/demo/SpringTransactionGetResult.java index 40056de5c..27ede4aaf 100644 --- a/src/main/java/com/example/demo/SpringTransactionGetResult.java +++ b/src/main/java/com/example/demo/SpringTransactionGetResult.java @@ -24,8 +24,8 @@ public CoreTransactionGetResult getInner() { @Override public String toString() { return "SpringTransactionGetResult{" + - "value=" + value + - ", inner=" + inner + - '}'; + "value=" + value + + ", inner=" + inner + + '}'; } } diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index 17797b5e9..70a6c9227 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -77,7 +77,7 @@ public interface CouchbaseClientFactory extends Closeable { PersistenceExceptionTranslator getExceptionTranslator(); CoreTransactionAttemptContext getCore(TransactionOptions options, - CoreTransactionAttemptContext atr); + CoreTransactionAttemptContext atr); //CouchbaseClientFactory with(CouchbaseTransactionalOperator txOp); diff --git a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java index 75a7acd2f..13f0f9773 100644 --- a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java @@ -23,7 +23,6 @@ import com.couchbase.client.java.Scope; import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; import com.couchbase.client.java.transactions.config.TransactionOptions; -import org.springframework.data.couchbase.transaction.ClientSessionOptions; import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; @@ -107,8 +106,7 @@ public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider* void close() throws IOException; - ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, - CoreTransactionAttemptContext ctx); + ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, CoreTransactionAttemptContext ctx); /* * (non-Javadoc) diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index d9b067751..f6d702447 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -53,21 +53,21 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { //private JsonSerializer serializer = null; public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName) { + final String bucketName) { this(connectionString, authenticator, bucketName, null); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName, final String scopeName) { + final String bucketName, final String scopeName) { this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator) - // todo gp disabling cleanupLostAttempts to simplify output during development - .environment(env -> env.transactionsConfig( - TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)))))), + // todo gp disabling cleanupLostAttempts to simplify output during development + .environment(env -> env.transactionsConfig( + TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)))))), bucketName, scopeName); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName, final String scopeName, final ClusterEnvironment environment) { + final String bucketName, final String scopeName, final ClusterEnvironment environment) { this( new OwnedSupplier<>( Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator).environment(environment))), @@ -81,7 +81,7 @@ public SimpleCouchbaseClientFactory(final Cluster cluster, final String bucketNa } private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, - final String scopeName) { + final String scopeName) { this.cluster = cluster; this.bucket = cluster.get().bucket(bucketName); this.scope = scopeName == null ? bucket.defaultScope() : bucket.scope(scopeName); diff --git a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java index ffd20b85a..5e90fc7e7 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java @@ -38,7 +38,7 @@ public class SimpleReactiveCouchbaseClientFactory implements ReactiveCouchbaseCl CouchbaseTransactionalOperator transactionalOperator; public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName, - CouchbaseTransactionalOperator transactionalOperator) { + CouchbaseTransactionalOperator transactionalOperator) { this.cluster = Mono.just(cluster); this.theCluster = cluster; this.bucketName = bucketName; @@ -146,15 +146,13 @@ public void close() { } @Override - public Mono getTransactionResources(TransactionOptions options) { // hopefully this - // gets filled in - // later - return Mono.just(new ReactiveCouchbaseResourceHolder(null)); + public Mono getTransactionResources(TransactionOptions options) { + return Mono.just(new ReactiveCouchbaseResourceHolder(null)); } @Override public ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, - CoreTransactionAttemptContext atr) { + CoreTransactionAttemptContext atr) { if (atr == null) { atr = AttemptContextReactiveAccessor .newCoreTranactionAttemptContext(AttemptContextReactiveAccessor.reactive(transactions)); @@ -218,7 +216,7 @@ static final class CoreTransactionAttemptContextBoundCouchbaseClientFactory // private final Transactions transactions; CoreTransactionAttemptContextBoundCouchbaseClientFactory(ReactiveCouchbaseResourceHolder transactionResources, - ReactiveCouchbaseClientFactory delegate, Transactions transactions) { + ReactiveCouchbaseClientFactory delegate, Transactions transactions) { this.transactionResources = transactionResources; this.delegate = delegate; // this.transactions = transactions; @@ -308,7 +306,7 @@ public Mono getTransactionResources(Transaction @Override public ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, - CoreTransactionAttemptContext atr) { + CoreTransactionAttemptContext atr) { ReactiveCouchbaseResourceHolder holder = delegate.getTransactionResources(options, atr); return holder; } diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 2f8ce5196..80f4e72b9 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -168,6 +168,7 @@ public ClusterEnvironment couchbaseClusterEnvironment() { throw new CouchbaseException("non-shadowed Jackson not present"); } builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper())); + // todo gp only suitable for tests TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)); builder.transactionsConfig(transactionsConfig()); configureEnvironment(builder); @@ -185,15 +186,15 @@ protected void configureEnvironment(final ClusterEnvironment.Builder builder) { @Bean(name = BeanNames.COUCHBASE_TEMPLATE) public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { return new CouchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, getDefaultConsistency()); } public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { return couchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); } @@ -291,7 +292,7 @@ public String typeKey() { */ @Bean public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext, - CouchbaseCustomConversions couchbaseCustomConversions) { + CouchbaseCustomConversions couchbaseCustomConversions) { MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, typeKey()); converter.setCustomConversions(couchbaseCustomConversions); return converter; @@ -346,12 +347,6 @@ public ObjectMapper couchbaseObjectMapper() { /***** ALL THIS TX SHOULD BE MOVED OUT INTO THE IMPL OF AbstractCouchbaseConfiguration *****/ - // todo gp how to DI this into the Cluster creation esp. as it creates a CoreTransactionConfig -// @Bean -// public TransactionsConfig transactionConfig() { -// return TransactionsConfig.builder().build(); -// } - @Bean(BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) ReactiveCouchbaseTransactionManager reactiveTransactionManager( ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) { @@ -377,11 +372,13 @@ CouchbaseTransactionManager transactionManager(CouchbaseClientFactory clientFact return new CouchbaseTransactionManager(clientFactory, options); } + // todo gpx these would be per-transactions options so it seems odd to have a global bean? Surely would want to configure everything at global level instead? @Bean public TransactionOptions transactionsOptions(){ return TransactionOptions.transactionOptions(); } + // todo gpx transactions config is now done in standard ClusterConfig - so I think we don't want a separate bean? public TransactionsConfig.Builder transactionsConfig(){ return TransactionsConfig.builder().durabilityLevel(DurabilityLevel.NONE).timeout(Duration.ofMinutes(20));// for testing } diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 179729fec..8164f2cd2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -58,7 +58,7 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv abstract ReactiveCouchbaseTemplate getReactiveTemplate(); public T decodeEntityBase(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { final CouchbaseDocument converted = new CouchbaseDocument(id); converted.setId(id); @@ -127,7 +127,7 @@ CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index d0c2b3370..8cec31313 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -55,6 +55,6 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations { QueryScanConsistency getConsistency(); T save(T entity); - Long count(Query query, Class domainType); + Long count(Query query, Class domainType); } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 60c9d8c9c..322bf4f73 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -59,19 +59,19 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex private CouchbaseTransactionalOperator couchbaseTransactionalOperator; public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, - final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter) { + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter) { this(clientFactory, reactiveCouchbaseClientFactory, converter, new JacksonTranslationService()); } public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, - final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, CouchbaseConverter converter, - final TranslationService translationService) { + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, CouchbaseConverter converter, + final TranslationService translationService) { this(clientFactory, reactiveCouchbaseClientFactory, converter, translationService, null); } public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, - final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter, - final TranslationService translationService, QueryScanConsistency scanConsistency) { + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter, + final TranslationService translationService, QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.templateSupport = new CouchbaseTemplateSupport(this, converter, translationService); @@ -91,8 +91,8 @@ public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, public T save(T entity) { if (hasNonZeroVersionProperty(entity, templateSupport.converter)) { return replaceById((Class) entity.getClass()).one(entity); - //} else if (getTransactionalOperator() != null) { - // return insertById((Class) entity.getClass()).one(entity); + //} else if (getTransactionalOperator() != null) { + // return insertById((Class) entity.getClass()).one(entity); } else { return upsertById((Class) entity.getClass()).one(entity); } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index a7252320e..7ce71a517 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -46,7 +46,7 @@ class CouchbaseTemplateSupport extends AbstractTemplateSupport implements Applic private EntityCallbacks entityCallbacks; public CouchbaseTemplateSupport(final CouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { super(template.reactive(), converter, translationService); this.template = template; } @@ -69,25 +69,25 @@ ReactiveCouchbaseTemplate getReactiveTemplate() { @Override public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txHolder) { + TransactionResultHolder txHolder) { return decodeEntity(id, source, cas, entityClass, scope, collection, txHolder); } @Override public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txHolder, ReactiveCouchbaseResourceHolder holder) { return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder); } @Override public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder) { + TransactionResultHolder txResultHolder) { return applyResult(entity, converted, id, cas,txResultHolder, null); } @Override public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, - TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { return applyResultBase(entity, converted, id, cas, txResultHolder, holder); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 87c9994ea..e17596292 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -52,7 +52,7 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, - GetOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx) { + GetOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index 581731eb1..f6219accb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -147,7 +147,7 @@ interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQuery /** * To be removed at the next major release. use WithConsistency instead - * + * * @param the entity type to use for the results. */ @Deprecated diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index 3339663f4..f793c8b94 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -64,9 +64,9 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final CouchbaseTransactionalOperator txCtx; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, - final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, - final QueryOptions options, final String[] distinctFields, final String[] fields, - final CouchbaseTransactionalOperator txCtx) { + final Query query, final QueryScanConsistency scanConsistency, final String scope, final String collection, + final QueryOptions options, final String[] distinctFields, final String[] fields, + final CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.returnType = returnType; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index f8f72611c..2fc06a1d2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -57,8 +57,8 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx) { + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index 2bb4ef4fa..c5a9e34f4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -62,8 +62,8 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas, CouchbaseTransactionalOperator txCtx) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas, CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index e087a9897..81007b7af 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -53,8 +53,8 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final CouchbaseTransactionalOperator txCtx; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, - CouchbaseTransactionalOperator txCtx) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.query = query; diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index cea9174f7..4aa8a39c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -100,6 +100,7 @@ interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExp } interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { + // todo gpx is this staying? It's confusing when doing ops.replaceById() inside @Transactional to get this transaction() method - unclear as a user whether I need to call it or not @Override TerminatingReplaceById transaction(CouchbaseTransactionalOperator txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index 85b78f610..751a5bf7a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -57,8 +57,8 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx) { + final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index aee240adb..2020a0d43 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -43,25 +43,25 @@ public Mono encodeEntity(Object entityToEncode) { @Override public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder) { + TransactionResultHolder txResultHolder) { return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder) { + TransactionResultHolder txResultHolder) { return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder)); } @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder, holder)); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index 75a4f1f1b..4450da160 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -76,13 +76,13 @@ public CouchbaseTransactionalOperator txOperator() { } public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, - final CouchbaseConverter converter) { + final CouchbaseConverter converter) { this(clientFactory, converter, new JacksonTranslationService(), null); } public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, - final CouchbaseConverter converter, final TranslationService translationService, - final QueryScanConsistency scanConsistency) { + final CouchbaseConverter converter, final TranslationService translationService, + final QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); @@ -278,10 +278,10 @@ protected Mono doGetTemplate() { /* private Flux withSession(ReactiveSessionCallback action, ClientSession session) { - + ReactiveSessionBoundCouchbaseTemplate operations = new ReactiveSessionBoundCouchbaseTemplate(session, ReactiveCouchbaseTemplate.this); - + return Flux.from(action.doInSession(operations)) // .contextWrite(ctx -> ReactiveMongoContext.setSession(ctx, Mono.just(session))); } @@ -303,7 +303,7 @@ public ReactiveCouchbaseOperations withCore(ReactiveCouchbaseResourceHolder core public ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions) { return withSession(mongoDatabaseFactory.getSession(sessionOptions)); } - + */ /** diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 34f7edcd8..7d73839c6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -47,7 +47,7 @@ class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport private ReactiveEntityCallbacks reactiveEntityCallbacks; public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { super(template, converter, translationService); this.template = template; } @@ -70,26 +70,26 @@ ReactiveCouchbaseTemplate getReactiveTemplate() { @Override public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder) { + TransactionResultHolder txResultHolder) { return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder) { + TransactionResultHolder txResultHolder) { return applyResult(entity, converted, id, cas, txResultHolder, null); } @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, holder)); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index 17eab98df..778299930 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -73,7 +73,9 @@ static class ReactiveExistsByIdSupport implements ReactiveExistsById { public Mono one(final String id) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, null, domainType); LOG.trace("existsById {}", pArgs); - return Mono.just(id) + + return TransactionalSupport.verifyNotInTransaction(template.doGetTemplate(), "existsById") + .then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getBlockingCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) .map(ExistsResult::exists)) diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index e429d0d19..ea47bc77e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -109,8 +109,9 @@ public Mono first() { public Flux all() { return Flux.defer(() -> { String statement = assembleEntityQuery(false); - return template.getCouchbaseClientFactory().getBlockingCluster().reactive() - .analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> { + return TransactionalSupport.verifyNotInTransaction(template.doGetTemplate(), "findByAnalytics") + .then(template.getCouchbaseClientFactory().getCluster().block().reactive() + .analyticsQuery(statement, buildAnalyticsOptions())).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index e7e3afb3f..475b719cc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -25,6 +25,7 @@ import reactor.core.publisher.Mono; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.Collection; @@ -71,8 +72,8 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { private final Duration expiry; ReactiveFindByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, - CommonOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx, - ReactiveTemplateSupport support) { + CommonOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -101,32 +102,24 @@ public Mono one(final String id) { .flatMap(s -> { System.err.println("Session: "+s); //Mono reactiveEntity = Mono.defer(() -> { - if (s == null || s.getCore() == null) { - if (pArgs.getOptions() instanceof GetAndTouchOptions) { - return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, - pArgs.getScope(), pArgs.getCollection(), null)); - } else { - return rc.get(id, (GetOptions) pArgs.getOptions()) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, - pArgs.getScope(), pArgs.getCollection(), null)); - } - } else { - return s.getCore().get(makeCollectionIdentifier(rc.async()), id) - .flatMap( result -> { - - // todo gp no cas // todo mr - it's required by replace().one when comparing to internal.cas(). it's gone - // todo gp if we need this of course needs to be exposed nicely - Long cas = result.cas(); - return support.decodeEntity(id, new String(result.contentAsBytes()), cas, domainType, pArgs.getScope(), - pArgs.getCollection(), new TransactionResultHolder(result), null).doOnNext(out -> { - // todo gp is this safe? are we on the right thread? - // org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(out, - // result); - }); - }); - } - })); + if (s == null || s.getCore() == null) { + if (pArgs.getOptions() instanceof GetAndTouchOptions) { + return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); + } else { + return rc.get(id, (GetOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); + } + } else { + return s.getCore().get(makeCollectionIdentifier(rc.async()), id) + .flatMap( result -> { + return support.decodeEntity(id, new String(result.contentAsBytes(), StandardCharsets.UTF_8), result.cas(), domainType, pArgs.getScope(), + pArgs.getCollection(), new TransactionResultHolder(result), null); + }); + } + })); return reactiveEntity.onErrorResume(throwable -> { if (throwable instanceof DocumentNotFoundException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index a46d94944..89df6ae9c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -95,7 +95,7 @@ interface TerminatingFindByQuery extends OneAndAllReactive { /** * Fluent method to specify options. - * + * * @param the entity type to use for the results. */ interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 95d027609..8e0ccf310 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -76,9 +76,9 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, - final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, - final CouchbaseTransactionalOperator txCtx, final ReactiveTemplateSupport support) { + final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, + final String collection, final QueryOptions options, final String[] distinctFields, String[] fields, + final CouchbaseTransactionalOperator txCtx, final ReactiveTemplateSupport support) { Assert.notNull(domainType, "domainType must not be null!"); Assert.notNull(returnType, "returnType must not be null!"); this.template = template; @@ -99,7 +99,7 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { public FindByQueryWithQuery matching(Query query) { QueryScanConsistency scanCons; if (query.getScanConsistency() != null) { // redundant, since buildQueryOptions() will use - // query.getScanConsistency() + // query.getScanConsistency() scanCons = query.getScanConsistency(); } else { scanCons = scanConsistency; @@ -196,12 +196,12 @@ public Flux all() { Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { if (s.getCore() == null) { QueryOptions opts = buildOptions(pArgs.getOptions()); - return pArgs.getScope() == null ? clientFactory.getBlockingCluster().reactive().query(statement, opts) + return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), - clientFactory.getBlockingCluster().environment().jsonSerializer()).query(statement, opts); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getCluster().block().environment().jsonSerializer())).query(statement, opts); } })); @@ -213,27 +213,27 @@ public Flux all() { } }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())).flatMap(row -> { - String id = ""; - long cas = 0; - if (!query.isDistinct() && distinctFields == null) { - if (row.getString(TemplateUtils.SELECT_ID) == null) { - return Flux.error(new CouchbaseException("query did not project " + TemplateUtils.SELECT_ID - + ". Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_ID + " and " - + TemplateUtils.SELECT_CAS + " : " + statement)); - } - id = row.getString(TemplateUtils.SELECT_ID); - if (row.getLong(TemplateUtils.SELECT_CAS) == null) { - return Flux.error(new CouchbaseException("query did not project " + TemplateUtils.SELECT_CAS - + ". Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_ID + " and " - + TemplateUtils.SELECT_CAS + " : " + statement)); - } - cas = row.getLong(TemplateUtils.SELECT_CAS); - row.removeKey(TemplateUtils.SELECT_ID); - row.removeKey(TemplateUtils.SELECT_CAS); - } - return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), - null); - }); + String id = ""; + long cas = 0; + if (!query.isDistinct() && distinctFields == null) { + if (row.getString(TemplateUtils.SELECT_ID) == null) { + return Flux.error(new CouchbaseException("query did not project " + TemplateUtils.SELECT_ID + + ". Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_ID + " and " + + TemplateUtils.SELECT_CAS + " : " + statement)); + } + id = row.getString(TemplateUtils.SELECT_ID); + if (row.getLong(TemplateUtils.SELECT_CAS) == null) { + return Flux.error(new CouchbaseException("query did not project " + TemplateUtils.SELECT_CAS + + ". Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_ID + " and " + + TemplateUtils.SELECT_CAS + " : " + statement)); + } + cas = row.getLong(TemplateUtils.SELECT_CAS); + row.removeKey(TemplateUtils.SELECT_ID); + row.removeKey(TemplateUtils.SELECT_CAS); + } + return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), + null); + }); } public QueryOptions buildOptions(QueryOptions options) { @@ -263,19 +263,19 @@ public Mono count() { : rs.query(statement, opts); } else { TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); - return AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), - clientFactory.getBlockingCluster().environment().jsonSerializer()).query(statement, opts); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getBlockingCluster().environment().jsonSerializer())).query(statement, opts); } })); return allResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() - : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())) + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject() + : Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())) .map(row -> row.getLong(row.getNames().iterator().next())).elementAt(0); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 1be069939..cadca3839 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -75,7 +75,8 @@ public Mono any(final String id) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, null, domainType); LOG.trace("getAnyReplica {}", pArgs); - return Mono.just(id) + return TransactionalSupport.verifyNotInTransaction(template.doGetTemplate(), "findFromReplicasById") + .then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getBlockingCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 6bddbbb0f..4de3a5f3c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.core; import com.couchbase.client.core.transaction.CoreTransactionGetResult; -import com.couchbase.client.java.codec.Transcoder; -import com.couchbase.client.java.transactions.TransactionGetResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -74,9 +72,9 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { private final ReactiveTemplateSupport support; ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, CouchbaseTransactionalOperator txCtx, - ReactiveTemplateSupport support) { + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Duration expiry, CouchbaseTransactionalOperator txCtx, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -92,9 +90,9 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { } ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, TransactionalOperator txOp, - ReactiveTemplateSupport support) { + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Duration expiry, TransactionalOperator txOp, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -116,18 +114,34 @@ public Mono one(T object) { System.err.println("txOp: " + pArgs.getTxOp()); Mono tmpl = template.doGetTemplate(); - return GenericSupport.one(tmpl, pArgs.getTxOp(), pArgs.getScope(), pArgs.getCollection(), support, object, - (GenericSupportHelper support) -> support.collection + return TransactionalSupport.one(tmpl, pArgs.getTxOp(), pArgs.getScope(), pArgs.getCollection(), support, object, + (TransactionalSupportHelper support) -> support.collection .insert(support.converted.getId(), support.converted.export(), buildOptions(pArgs.getOptions(), support.converted)) .flatMap(result -> this.support.applyResult(object, support.converted, support.converted.getId(), result.cas(), null)), - (GenericSupportHelper support) -> support.ctx - .insert(makeCollectionIdentifier(support.collection.async()), support.converted.getId(), - template.getCouchbaseClientFactory().getCluster().block().environment().transcoder() - .encode(support.converted.export()).encoded()) - .flatMap(result -> this.support.applyResult(object, support.converted, support.converted.getId(), - getCas(result), new TransactionResultHolder(result), null))); + (TransactionalSupportHelper support) -> { + rejectInvalidTransactionalOptions(); + + return support.ctx + .insert(makeCollectionIdentifier(support.collection.async()), support.converted.getId(), + template.getCouchbaseClientFactory().getBlockingCluster().environment().transcoder() + .encode(support.converted.export()).encoded()) + .flatMap(result -> this.support.applyResult(object, support.converted, support.converted.getId(), + getCas(result), new TransactionResultHolder(result), null)); + }); + } + + private void rejectInvalidTransactionalOptions() { + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + } + if (this.expiry != null) { + throw new IllegalArgumentException("withExpiry is not supported in a transaction"); + } + if (this.options != null) { + throw new IllegalArgumentException("withOptions is not supported in a transaction"); + } } private Long getCas(CoreTransactionGetResult getResult) { @@ -177,7 +191,7 @@ public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel durabilityLevel, expiry, txCtx, support); } - // todo gp need to figure out how to handle options re transactions. E.g. many non-transactional insert options, + // todo gpx need to figure out how to handle options re transactions. E.g. many non-transactional insert options, // like this, aren't supported @Override public InsertByIdInScope withDurability(final PersistTo persistTo, final ReplicateTo replicateTo) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 15489c3f1..fb8f3ee84 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -77,8 +77,8 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final CouchbaseTransactionalOperator txCtx; ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas, CouchbaseTransactionalOperator txCtx) { + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Long cas, CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -101,42 +101,47 @@ public Mono one(final String id) { Mono tmpl = template.doGetTemplate(); final Mono removeResult; + // todo gpx convert to TransactionalSupport Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { if (s.getCore() == null) { System.err.println("non-tx remove"); return rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r)); } else { + rejectInvalidTransactionalOptions(); + System.err.println("tx remove"); - // todo gp we definitely don't want to be creating TransactionGetResult. It's essential that this is passed - // from a previous ctx.get(). So we know if this doc is in a transaction and can safely detect - // write-write conflicts. This will be a blocker. - // Looks like replace is solving this with a getTransactionHolder? if ( cas == null || cas == 0 ){ throw new IllegalArgumentException("cas must be supplied for tx remove"); } Mono gr = s.getCore().get(makeCollectionIdentifier(rc.async()), id); - // todo gp no CAS return gr.flatMap(getResult -> { - if (getResult.cas() != cas) { - System.err.println("internal: "+getResult.cas()+" object.cas: "+cas); - // todo gp really want to set internal state and raise a TransactionOperationFailed - return Mono.error(new TransactionOperationFailedException(true, true, new CasMismatchException(null), null)); + if (getResult.cas() != cas) { + return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(s.getCore(), getResult.cas(), cas)); } return s.getCore().remove(getResult) .map(r -> new RemoveResult(id, 0, null)); }); }}).onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - })); + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + })); return allResult; } + private void rejectInvalidTransactionalOptions() { + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + } + if (this.options != null) { + throw new IllegalArgumentException("withOptions is not supported in a transaction"); + } + } + @Override public Mono oneEntity(Object entity) { ReactiveRemoveByIdSupport op = new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 766b6b9dd..95aed499c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -92,8 +92,8 @@ interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, W /** * Provide the transaction * - * @param txCtx - transaction - */ + * @param txCtx - transaction + */ @Override TerminatingRemoveByQuery transaction(CouchbaseTransactionalOperator txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index 5d08448a5..d21bcab01 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -64,8 +64,8 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final CouchbaseTransactionalOperator txCtx; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, - CouchbaseTransactionalOperator txCtx) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.query = query; @@ -91,16 +91,16 @@ public Flux all() { } else { TransactionQueryOptions opts = buildTransactionOptions(buildQueryOptions(pArgs.getOptions())); Mono tqr = pArgs.getScope() == null ? pArgs.getTxOp().getAttemptContextReactive().query(statement, opts) : pArgs.getTxOp().getAttemptContextReactive().query(rs, statement, opts); - // todo gp do something with tqr + // todo gpx do something with tqr } Mono finalAllResult = allResult; return Flux.defer(() -> finalAllResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject) + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject) .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), Optional.empty()))); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index c18b66c76..deda7dd9b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -50,7 +50,7 @@ public interface ReactiveReplaceByIdOperation { */ ReactiveReplaceById replaceById(Class domainType); - /** + /** * Terminating operations invoking the actual execution. */ interface TerminatingReplaceById extends OneAndAllEntityReactive { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index f03e54d14..383ed9c5f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -20,6 +20,10 @@ import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import com.couchbase.client.core.transaction.CoreTransactionGetResult; import com.couchbase.client.java.transactions.TransactionGetResult; +import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.core.transaction.util.DebugUtil; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -74,9 +78,9 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final ReactiveTemplateSupport support; ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx, - ReactiveTemplateSupport support) { + final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, final CouchbaseTransactionalOperator txCtx, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -96,46 +100,29 @@ public Mono one(T object) { LOG.trace("replaceById {}", pArgs); Mono tmpl = template.doGetTemplate(); - return GenericSupport.one(tmpl, pArgs.getTxOp(), pArgs.getScope(), pArgs.getCollection(), support, object, - (GenericSupportHelper support) -> { + return TransactionalSupport.one(tmpl, pArgs.getTxOp(), pArgs.getScope(), pArgs.getCollection(), support, object, + (TransactionalSupportHelper support) -> { CouchbaseDocument converted = support.converted; return support.collection .replace(converted.getId(), converted.export(), buildReplaceOptions(pArgs.getOptions(), object, converted)) .flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), null)); - }, (GenericSupportHelper support) -> { + }, (TransactionalSupportHelper support) -> { + rejectInvalidTransactionalOptions(); + CouchbaseDocument converted = support.converted; if ( support.cas == null || support.cas == 0 ){ throw new IllegalArgumentException("cas must be supplied in object for tx replace. object="+object); } - // todo gp replace is a nightmare... - // Where to put and how to pass the TransactionGetResult - // - Idea: TransactionSynchronizationManager.bindResource - // - Idea: use @Version as an index into Map - // - As below, one idea is not to store it at all. - // Person could have been fetched outside of @Transactional block. Need to flat out prevent. Right?? - // - Maybe not. Could have the replaceById do a ctx.get(), and check the CAS matches the Person (will - // mandate @Version on Person). - // - Could always do that in fact. Then no need to hold onto TransactionGetResult anywhere - but slower too - // (could optimise later). - // - And if had get-less replaces, could pass in the CAS. - // - Note: if Person was fetched outside the transaction, the transaction will inevitably expire (continuous - // CAS mismatch). - // -- Will have to doc that the user generally wants to do the read inside the txn. - // -- Can we detect this scenario and reject at runtime? That would also probably need storing something in - // Person. - - // TransactionGetResult gr = (TransactionGetResult) - // org.springframework.transaction.support.TransactionSynchronizationManager.getResource(object); - Mono gr = support.ctx.get(makeCollectionIdentifier(support.collection.async()), converted.getId()); - - // todo gp no CAS + + CollectionIdentifier collId = makeCollectionIdentifier(support.collection.async()); + support.ctx.logger().info(support.ctx.attemptId(), "refetching %s for Spring replace", DebugUtil.docId(collId, converted.getId())); + Mono gr = support.ctx.get(collId, converted.getId()); + return gr.flatMap(getResult -> { - if (getResult.cas() != support.cas) { - System.err.println("internal: "+getResult.cas()+" object.cas: "+ support.cas+" "+converted); - // todo gp really want to set internal state and raise a TransactionOperationFailed - return Mono.error(new TransactionOperationFailedException(true, true, new CasMismatchException(null), null)); + if (getResult.cas() != support.cas) { + return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(support.ctx, getResult.cas(), support.cas)); } return support.ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().block().environment().transcoder() .encode(support.converted.export()).encoded()); @@ -144,18 +131,16 @@ public Mono one(T object) { } - private Integer getTransactionHolder(T object) { - Integer transactionResultHolder; - System.err.println("GET: " + System.identityHashCode(object) + " " + object); - if (1 == 1) { - return System.identityHashCode(object); + private void rejectInvalidTransactionalOptions() { + if ((this.persistTo != null && this.persistTo != PersistTo.NONE) || (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) { + throw new IllegalArgumentException("withDurability PersistTo and ReplicateTo overload is not supported in a transaction"); + } + if (this.expiry != null) { + throw new IllegalArgumentException("withExpiry is not supported in a transaction"); } - transactionResultHolder = template.support().getTxResultHolder(object); - if (transactionResultHolder == null) { - throw new CouchbaseException( - "TransactionResult from entity is null - was the entity obtained in a transaction?"); + if (this.options != null) { + throw new IllegalArgumentException("withOptions is not supported in a transaction"); } - return transactionResultHolder; } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 7dcdcb570..1313a646d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -30,13 +30,13 @@ public interface ReactiveTemplateSupport { Mono encodeEntity(Object entityToEncode); Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, - TransactionResultHolder txResultHolder); + TransactionResultHolder txResultHolder); Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, - TransactionResultHolder txResultHolder); + TransactionResultHolder txResultHolder); Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index fd0c3b5f0..32949e2c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -64,8 +64,8 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { private final ReactiveTemplateSupport support; ReactiveUpsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final UpsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { + final String collection, final UpsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, final Duration expiry, ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -83,17 +83,14 @@ public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, null, domainType); LOG.trace("upsertById {}", pArgs); Mono tmpl = template.doGetTemplate(); - Mono reactiveEntity = support.encodeEntity(object) - .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { - if (s.getCore() == null) { - return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).flatMap(collection -> collection.reactive() - .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); - } else { - return Mono.error(new CouchbaseException("No upsert in a transaction. Use insert or replace")); - } - }))); + Mono reactiveEntity = TransactionalSupport.verifyNotInTransaction(template.doGetTemplate(), "upsertById") + .then(support.encodeEntity(object)) + .flatMap(converted -> tmpl.flatMap(tp -> { + return tp.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).flatMap(collection -> collection.reactive() + .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) + .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null))); + })); return reactiveEntity.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 88edb0eb4..260d90419 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -47,5 +47,5 @@ public interface TemplateSupport { Integer getTxResultHolder(T source); - TranslationService getTranslationService(); + TranslationService getTranslationService(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java new file mode 100644 index 000000000..37d5c22dd --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/TransactionalSupport.java @@ -0,0 +1,95 @@ +package org.springframework.data.couchbase.core; + +import com.couchbase.client.core.error.CasMismatchException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Method; +import java.util.function.Function; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.lang.Nullable; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.java.ReactiveCollection; + +@Stability.Internal +class TransactionalSupportHelper { + public final CouchbaseDocument converted; + public final Long cas; + public final ReactiveCollection collection; + public final @Nullable CoreTransactionAttemptContext ctx; + + public TransactionalSupportHelper(CouchbaseDocument doc, Long cas, ReactiveCollection collection, + @Nullable CoreTransactionAttemptContext ctx) { + this.converted = doc; + this.cas = cas; + this.collection = collection; + this.ctx = ctx; + } +} + +/** + * Checks if this operation is being run inside a transaction, and calls a non-transactional or transactional callback + * as appropriate. + */ +@Stability.Internal +public class TransactionalSupport { + public static Mono one(Mono tmpl, CouchbaseTransactionalOperator transactionalOperator, + String scopeName, String collectionName, ReactiveTemplateSupport support, T object, + Function> nonTransactional, Function> transactional) { + return tmpl.flatMap(template -> template.getCouchbaseClientFactory().withScope(scopeName) + .getCollection(collectionName).flatMap(collection -> support.encodeEntity(object) + .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + TransactionalSupportHelper gsh = new TransactionalSupportHelper(converted, support.getCas(object), + collection.reactive(), s.getCore() != null ? s.getCore() + : (transactionalOperator != null ? transactionalOperator.getAttemptContext() : null)); + if (gsh.ctx == null) { + System.err.println("non-tx"); + return nonTransactional.apply(gsh); + } else { + System.err.println("tx"); + return transactional.apply(gsh); + } + })).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + })))); + } + + public static Mono verifyNotInTransaction(Mono tmpl, String methodName) { + return tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null) + .flatMap(s -> { + if (s.hasActiveTransaction()) { + return Mono.error(new IllegalArgumentException(methodName + "can not be used inside a transaction")); + } + else { + return Mono.empty(); + } + })); + } + + public static RuntimeException retryTransactionOnCasMismatch(CoreTransactionAttemptContext ctx, long cas1, long cas2) { + try { + ctx.logger().info(ctx.attemptId(), "Spring CAS mismatch %s != %s, retrying transaction", cas1, cas2); + + // todo gpx expose this in SDK + Method method = CoreTransactionAttemptContext.class.getDeclaredMethod("operationFailed", TransactionOperationFailedException.class); + method.setAccessible(true); + TransactionOperationFailedException err = TransactionOperationFailedException.Builder.createError() + .retryTransaction() + .cause(new CasMismatchException(null)) + .build(); + method.invoke(ctx, err); + return err; + } catch (Throwable err) { + return new RuntimeException(err); + } + + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index 9d038bf39..3a5ef7a7d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -43,7 +43,7 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options, Couchba * 1) values from fluent api
    * 2) values from dynamic proxy (via template threadLocal)
    * 3) the values from the couchbaseClientFactory
    - * + * * @param template which holds ThreadLocal pseudo args * @param scope - from calling operation * @param collection - from calling operation @@ -51,7 +51,7 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options, Couchba * @param domainType - entity that may have annotations */ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, - CouchbaseTransactionalOperator transactionalOperator, Class domainType) { + CouchbaseTransactionalOperator transactionalOperator, Class domainType) { String scopeForQuery = null; String collectionForQuery = null; diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java index 5662a6804..b5fd14bef 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java @@ -27,7 +27,7 @@ public interface WithTransaction { /** * Specify transactions * - * @param txCtx - */ + * @param txCtx + */ Object transaction(CouchbaseTransactionalOperator txCtx); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java index 6b190091d..fbbe66ad7 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -28,7 +28,7 @@ * The generic parameter needs to be REPO which is either a CouchbaseRepository parameterized on T,ID or a * ReactiveCouchbaseRepository parameterized on T,ID. i.e.: interface AirportRepository extends * CouchbaseRepository<Airport, String>, DynamicProxyable<AirportRepository> - * + * * @param * @author Michael Reiche */ diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index b54777322..e24e10dbd 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -37,7 +37,7 @@ public class CouchbaseRepositoryBase { private CrudMethodMetadata crudMethodMetadata; public CouchbaseRepositoryBase(CouchbaseEntityInformation entityInformation, - Class repositoryInterface) { + Class repositoryInterface) { this.entityInformation = entityInformation; this.repositoryInterface = repositoryInterface; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index d37e45552..9c0032854 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -49,7 +49,7 @@ public class DynamicInvocationHandler implements InvocationHandler { CouchbaseTransactionalOperator ctx; public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope, - CouchbaseTransactionalOperator ctx) { + CouchbaseTransactionalOperator ctx) { this.target = target; if (target instanceof CouchbaseRepository) { reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 1cc074fae..0e4a28032 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -63,7 +63,7 @@ public class SimpleReactiveCouchbaseRepository extends CouchbaseRepositor * @param operations the reference to the reactive template used. */ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation entityInformation, - ReactiveCouchbaseOperations operations, Class repositoryInterface) { + ReactiveCouchbaseOperations operations, Class repositoryInterface) { super(entityInformation, repositoryInterface); this.operations = operations; } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java b/src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java deleted file mode 100644 index 7bab2a867..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/AbortCommitSubscriber.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.springframework.data.couchbase.transaction; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import java.util.concurrent.Semaphore; - -class AbortCommitSubscriber implements Subscriber { - private Subscription subscription; - private final String name; - private final Semaphore lock; - - public AbortCommitSubscriber(String name){ - this.name = name; - this.lock = new Semaphore(1); - try { - lock.acquire(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - /** - * This method is triggered when the Subscriber subscribes to a Publisher - */ - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } - - /** - * This method is triggered the Subscriber receives an event - * signaling an item being sent from the publisher. The Item is simply printed here. - */ - @Override - public void onNext(T item) { - subscription.request(1); - } - /** - * This method is triggered when the Subscriber receives an error event. - * In our case we just print the error message. - */ - @Override - public void onError(Throwable error) { - System.err.println(name + " Error Occurred: " + error.getMessage()); - } - /** - * This method is triggered when the Subscriber Receives a complete. This means - * it has already received and processed all items from the publisher to which it is subscribed. - */ - @Override - public void onComplete() { - lock.release(); - } - - public Semaphore getLock() { - return lock; - } - - public void waitUntilComplete() { - try { - lock.acquire(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImplx.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImplx.java deleted file mode 100644 index 3aa6b29d8..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionImplx.java +++ /dev/null @@ -1,279 +0,0 @@ -package org.springframework.data.couchbase.transaction; - -import com.couchbase.client.core.transaction.support.AttemptState; -import com.couchbase.client.core.transaction.support.TransactionAttemptContextFactory; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import com.couchbase.client.java.transactions.TransactionAttemptContext; -import com.couchbase.client.java.transactions.Transactions; -import com.couchbase.client.java.transactions.config.TransactionOptions; -import com.couchbase.client.java.transactions.config.TransactionsConfig; -import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; -import org.springframework.transaction.reactive.TransactionContext; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -import org.reactivestreams.Publisher; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import org.springframework.util.Assert; - -import com.couchbase.client.core.error.CouchbaseException; -import com.couchbase.client.java.AsyncCluster; -import com.couchbase.client.java.Scope; -import com.couchbase.client.java.env.ClusterEnvironment; - -public class ClientSessionImplx implements ClientSessionx { - - protected transient Log logger = LogFactory.getLog(AbstractReactiveTransactionManager.class); - - Mono scopeRx; - Scope scope; - boolean commitInProgress = false; - boolean messageSentInCurrentTransaction = true; // needs to be true for commit - // todo gp probably should not be duplicating CoreTransactionAttemptContext state outside of it - //AttemptState transactionState = AttemptState.NOT_STARTED; - TransactionOptions transactionOptions; - TransactionContext ctx; - ReactiveTransactionAttemptContext atr = null; - Map getResultMap = new HashMap<>(); - - public ClientSessionImplx(){} - - public ClientSessionImplx(ReactiveCouchbaseClientFactory couchbaseClientFactory, ReactiveTransactionAttemptContext atr) { - this.scopeRx = couchbaseClientFactory.getScope(); - this.atr = atr; - System.err.println("new "+this); - } - - public ClientSessionImplx(CouchbaseClientFactory couchbaseClientFactory, ReactiveTransactionAttemptContext atr) { - this.scope = couchbaseClientFactory.getScope(); - this.atr = atr; - System.err.println("NEW "+this); - } - - private Transactions getTransactions(Transactions transactions) { - return transactions; - } - - @Override - public Mono getScope() { - return scopeRx; - } - - @Override - public boolean hasActiveTransaction() { - return false; - } - - @Override - public boolean notifyMessageSent() { - return false; - } - - @Override - public void notifyOperationInitiated(Object var1) { - - } - - @Override - public ReactiveTransactionAttemptContext getReactiveTransactionAttemptContext(){ - return atr; - } - - @Override - public TransactionAttemptContext getTransactionAttemptContext(){ - return atr == null? null : AttemptContextReactiveAccessor.blocking(atr); - } - - @Override - public TransactionOptions getTransactionOptions() { - return transactionOptions; - } - - @Override - public AsyncCluster getWrapped() { - return null; - } - - // todo gp - @Override - public void startTransaction() { - System.err.println("startTransaction: "+this); - //transactionState = AttemptState.PENDING; - } - - // todo gp - @Override - public Publisher commitTransaction() { - AttemptState state = getState(); - if (state == AttemptState.ABORTED) { - throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction"); - } else if (state == AttemptState.NOT_STARTED) { - throw new IllegalStateException("There is no transaction started"); - } else if (!this.messageSentInCurrentTransaction) { // seems there should have been a messageSent. We just do nothing(?) - this.cleanupTransaction(AttemptState.COMMITTED); - return Mono.create(MonoSink::success); - } else { - /*ReadConcern readConcern = this.transactionOptions.getReadConcern(); */ - if (0 == 1/* readConcern == null*/) { - throw new CouchbaseException("Invariant violated. Transaction options read concern can not be null"); - } else { - boolean alreadyCommitted = this.commitInProgress || state == AttemptState.COMMITTED; - this.commitInProgress = true; - // this will fail with ctx.serialized() being Optional.empty() - // how does the commit happen in transactions.reactive().run() ? - /* - return transactions.reactive().commit(ctx.serialized().get(), null).then().doOnSuccess(x -> { - commitInProgress = false; - this.transactionState = AttemptState.COMMITTED; - }).doOnError(CouchbaseException.class, this::clearTransactionContextOnError); - */ - // TODO MSR - // return Mono.create(MonoSink::success); - return executeImplicitCommit(atr).then(); //transactions.reactive().executeImplicitCommit(atr).then(); - /* - return this.executor.execute((new CommitTransactionOperation(this.transactionOptions.getWriteConcern(), alreadyCommitted)).recoveryToken(this.getRecoveryToken()).maxCommitTime(this.transactionOptions.getMaxCommitTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS), readConcern, this).doOnTerminate(() -> { - this.commitInProgress = false; - this.transactionState = AttemptState.COMMITTED; - }).doOnError(CouchbaseException.class, this::clearTransactionContextOnError); - - */ - } - - } - } - - public Mono executeImplicitCommit(ReactiveTransactionAttemptContext ctx) { - - if (logger.isDebugEnabled()) { - logger.debug(String.format("About to commit ctx %s", ctx)); - } - // If app has not explicitly performed a commit, assume they want to do so anyway - if (0 != 1 /*!ctx.isDone()*/) { - if (0 == 1 /*ctx.serialized().isPresent()*/) { - return Mono.empty(); // Mono.just(ctx); - } else { - //System.err.println(ctx.attemptId()+ " doing implicit commit"); // ctx.LOGGER.trace(); - System.err.println("doing implicit commit: "+this); - return AttemptContextReactiveAccessor.implicitCommit(atr, false); - - // todo gp ctx.commit() has gone in the SDK integration. Do we need this logic though? - //return Mono.empty(); -// if(ctx != null) { -// return ctx.commit() -// .then(Mono.just(ctx)) -// .onErrorResume(err -> Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, -// ctx))); -// } else { -// at.commit(); -// return Mono.empty(); -// } - } - } else { - System.err.println("Transaction already done"); - //System.err.println(ctx.attemptId()+" Transaction already done"); // // ctx.LOGGER.trace(); - return Mono.empty(); // Mono.just(ctx); - } - } - - - - @Override - public Publisher abortTransaction() { - System.err.println("**** abortTransaction ****"); -// Assert.notNull(transactions, "transactions"); -// Assert.notNull(ctx, "ctx"); -// Assert.notNull(ctx.serialized(), "ctx.serialized()"); -// if (ctx.serialized().isPresent()) { -// Assert.notNull(ctx.serialized().get(), "ctx.serialized().get()"); -// return transactions.reactive().rollback(ctx.serialized().get(), null).then(); -// } else { - return executeExplicitRollback(atr).then(); -// } - } - - private Mono executeExplicitRollback(ReactiveTransactionAttemptContext atr) { - // todo gp ctx.rollback() is removed - // todo mr - so what happens when the client requests that the tx be rolledback? - // todo mr - does throwing an exception result in rollback? - // todo mr - should an exception be thrown here on a request to rollback, when we can't do a rollback? - return Mono.empty(); - } - - @Override - public ServerSession getServerSession() { - return null; - } - - @Override - public void close() { - - } - - @Override - public Object getClusterTime() { - return null; - } - - @Override - public Object isCausallyConsistent() { - return null; - } - - private void cleanupTransaction(AttemptState attempState) {} - - private void clearTransactionContext() {} - - private void clearTransactionContextOnError(CouchbaseException e) { - String s = e.getMessage() != null ? e.getMessage().toLowerCase(Locale.ROOT) : null; - if (s != null && (s.contains("transienttransactionerror") || s.contains("unknowntransactioncommitresult"))) { - this.clearTransactionContext(); - } - - } - - @Override - public TransactionResultHolder transactionResultHolder(Integer key) { - TransactionResultHolder holder = getResultMap.get(key); - if(holder == null){ - throw new RuntimeException("did not find transactionResultHolder for key="+key+" in session"); - } - return holder; - } - - @Override - public TransactionResultHolder transactionResultHolder(TransactionResultHolder holder, Object o) { - System.err.println("PUT: "+System.identityHashCode(o)+" "+o); - getResultMap.put(System.identityHashCode(o), holder); - return holder; - } - - private static Duration now() { - return Duration.of(System.nanoTime(), ChronoUnit.NANOS); - } - - public String toString(){ - StringBuffer sb = new StringBuffer(); - sb.append(this.getClass().getSimpleName()+"@"+System.identityHashCode(this)); - sb.append("{"); - sb.append("atr: "+ ( atr == null ? null : atr.toString().replace("com.couchbase.client.java.transactions.",""))); - sb.append(", state: "+(atr == null ? null : getState())); - sb.append("}"); - return sb.toString(); - } - - private AttemptState getState() { - AttemptState state = AttemptContextReactiveAccessor.getState(atr); - return state != null ? state : AttemptState.NOT_STARTED; - } -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java deleted file mode 100644 index c5aa3417a..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionOptions.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.springframework.data.couchbase.transaction; - -import java.util.Objects; - -import com.couchbase.client.java.transactions.TransactionQueryOptions; -import org.springframework.data.annotation.Immutable; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -@Immutable -public final class ClientSessionOptions { - private final Boolean causallyConsistent; - private final Boolean snapshot; - private final TransactionQueryOptions defaultTransactionOptions; - - @Nullable - public Boolean isCausallyConsistent() { - return this.causallyConsistent; - } - - @Nullable - public Boolean isSnapshot() { - return this.snapshot; - } - - public TransactionQueryOptions getDefaultTransactionOptions() { - return this.defaultTransactionOptions; - } - - public boolean equals(Object o) { - if (this == o) { - return true; - } else if (o != null && this.getClass() == o.getClass()) { - ClientSessionOptions that = (ClientSessionOptions) o; - if (!Objects.equals(this.causallyConsistent, that.causallyConsistent)) { - return false; - } else if (!Objects.equals(this.snapshot, that.snapshot)) { - return false; - } else { - return Objects.equals(this.defaultTransactionOptions, that.defaultTransactionOptions); - } - } else { - return false; - } - } - - public int hashCode() { - int result = this.causallyConsistent != null ? this.causallyConsistent.hashCode() : 0; - result = 31 * result + (this.snapshot != null ? this.snapshot.hashCode() : 0); - result = 31 * result + (this.defaultTransactionOptions != null ? this.defaultTransactionOptions.hashCode() : 0); - return result; - } - - public String toString() { - return "ClientSessionOptions{causallyConsistent=" + this.causallyConsistent + "snapshot=" + this.snapshot - + ", defaultTransactionOptions=" + this.defaultTransactionOptions + '}'; - } - - public static ClientSessionOptions.Builder builder() { - return new ClientSessionOptions.Builder(); - } - - public static ClientSessionOptions.Builder builder(ClientSessionOptions options) { - Assert.notNull(options, "options"); - ClientSessionOptions.Builder builder = new ClientSessionOptions.Builder(); - builder.causallyConsistent = options.isCausallyConsistent(); - builder.snapshot = options.isSnapshot(); - builder.defaultTransactionOptions = options.getDefaultTransactionOptions(); - return builder; - } - - private ClientSessionOptions(ClientSessionOptions.Builder builder) { - if (builder.causallyConsistent != null && builder.causallyConsistent && builder.snapshot != null - && builder.snapshot) { - throw new IllegalArgumentException("A session can not be both a snapshot and causally consistent"); - } else { - this.causallyConsistent = builder.causallyConsistent == null && builder.snapshot != null ? !builder.snapshot - : builder.causallyConsistent; - this.snapshot = builder.snapshot; - this.defaultTransactionOptions = builder.defaultTransactionOptions; - } - } - - // @NotThreadSafe - public static final class Builder { - private Boolean causallyConsistent; - private Boolean snapshot; - private TransactionQueryOptions defaultTransactionOptions; - - public ClientSessionOptions.Builder causallyConsistent(boolean causallyConsistent) { - this.causallyConsistent = causallyConsistent; - return this; - } - - public ClientSessionOptions.Builder snapshot(boolean snapshot) { - this.snapshot = snapshot; - return this; - } - - public ClientSessionOptions.Builder defaultTransactionOptions(TransactionQueryOptions defaultTransactionOptions) { - Assert.notNull(defaultTransactionOptions, "defaultTransactionOptions"); - this.defaultTransactionOptions = defaultTransactionOptions; - return this; - } - - public ClientSessionOptions build() { - return new ClientSessionOptions(this); - } - - private Builder() { - /* TODO this.defaultTransactionOptions = TransactionQueryOptions.builder().build();*/ - } - } -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionx.java b/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionx.java deleted file mode 100644 index a09b8e0f6..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/ClientSessionx.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.springframework.data.couchbase.transaction; - - -import com.couchbase.client.java.AsyncCluster; -import com.couchbase.client.java.Scope; -import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; -import com.couchbase.client.java.transactions.TransactionAttemptContext; -import com.couchbase.client.java.transactions.config.TransactionOptions; -import org.reactivestreams.Publisher; -import org.springframework.data.couchbase.repository.support.TransactionResultHolder; -import reactor.core.publisher.Mono; - -/** - * ClientSession. There is only one implementation - ClientSessionImpl - * The SpringTransaction framework relies on the client session to perform commit() and abort() - * and therefore it has a ReactiveTransactionAttemptContext - * - * @author Michael Reiche - */ -// todo gp understand why this is needed -public interface ClientSessionx /*extends com.mongodb.session.ClientSession*/ { - - Mono getScope(); - - //Mono getScopeReactive(); - - boolean hasActiveTransaction(); - - boolean notifyMessageSent(); - - void notifyOperationInitiated(Object var1); - - //void setAttemptContextReactive(ReactiveTransactionAttemptContext atr); - - ReactiveTransactionAttemptContext getReactiveTransactionAttemptContext(); - - TransactionOptions getTransactionOptions(); - - AsyncCluster getWrapped(); - - void startTransaction(); - - Publisher commitTransaction(); - - Publisher abortTransaction(); - - ServerSession getServerSession(); - - void close(); - - Object getClusterTime(); - - Object isCausallyConsistent(); - - T transactionResultHolder(TransactionResultHolder result, T o); - - TransactionResultHolder transactionResultHolder(Integer key); - - TransactionAttemptContext getTransactionAttemptContext(); - - //ClientSession with(ReactiveTransactionAttemptContext atr); -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java deleted file mode 100644 index 4125ef42c..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseAttemptContextReactive.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2021 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.transaction; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.repository.DynamicProxyable; -import org.springframework.transaction.reactive.TransactionalOperator; - -import com.couchbase.client.core.error.CouchbaseException; -// import com.couchbase.transactions.ReactiveTransactionAttemptContext; - - -/** - * This is a proxy for ReactiveTransactionAttemptContext that also has the transactionalOperator, so that it can provide the - * transactionalOperator to the repository and templates used within the transaction lambda via ctx.template(templ) and - * ctx.repository(repo) - */ -public interface CouchbaseAttemptContextReactive { - - > R repository(R repo); - - ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template); - - static CouchbaseAttemptContextReactive proxyFor(/*ReactiveTransactionAttemptContext acr,*/ TransactionalOperator txOperator) { - Class[] interfaces = new Class[] { /* AttemptContextReactiveInterface.class, */ - CouchbaseAttemptContextReactive.class }; - CouchbaseAttemptContextReactive proxyInstance = (CouchbaseAttemptContextReactive) Proxy.newProxyInstance( - txOperator.getClass().getClassLoader(), interfaces, - new CouchbaseAttemptContextReactive.ACRInvocationHandler(/*acr,*/ txOperator)); - return proxyInstance; - } - - class ACRInvocationHandler implements InvocationHandler { - - // final ReactiveTransactionAttemptContext acr; - final TransactionalOperator txOperator; - - public ACRInvocationHandler(/*ReactiveTransactionAttemptContext acr,*/ TransactionalOperator txOperator) { -// this.acr = acr; - this.txOperator = txOperator; - } - - public ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template) { - ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseTransactionalOperator) txOperator) - .getTransactionManager()); - if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { - throw new CouchbaseException( - "Template must use the same clientFactory as the transactionManager of the transactionalOperator " - + template); - } - return template;//.with((CouchbaseStuffHandle) txOperator); // this returns a new template with a new - // couchbaseClient with txOperator - } - - public > R repository(R repo) { - if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) { - throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo); - } - ReactiveCouchbaseOperations reactiveOperations = (ReactiveCouchbaseOperations) repo.getOperations(); - ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseTransactionalOperator) txOperator) - .getTransactionManager()); - - if (reactiveOperations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { - throw new CouchbaseException( - "Repository must use the same clientFactory as the transactionManager of the transactionalOperator " - + repo); - } - return repo.withTransaction((CouchbaseTransactionalOperator) txOperator); // this returns a new repository proxy with txOperator in its threadLocal - // what if instead we returned a new repo with a new template with the txOperator? - } - - @Override - public Object invoke(Object o, Method method, Object[] objects) throws Throwable { - if (method.getName().equals("template")) { - return template((ReactiveCouchbaseTemplate) objects[0]); - } - if (method.getName().equals("repository")) { - return repository((DynamicProxyable) objects[0]); - } - throw new UnsupportedOperationException(method.toString()); - //return method.invoke(acr, objects); - } - } - -} 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 a8c0434d0..772ed2853 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java @@ -15,9 +15,11 @@ */ 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; @@ -39,11 +41,11 @@ 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; -// todo gp experimenting with simplest possible CallbackPreferringPlatformTransactionManager, extending PlatformTransactionManager -// not AbstractPlatformTransactionManager -public class CouchbaseSimpleCallbackTransactionManager /* extends AbstractPlatformTransactionManager*/ implements CallbackPreferringPlatformTransactionManager { +public class CouchbaseSimpleCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager { private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); @@ -59,31 +61,54 @@ public CouchbaseSimpleCallbackTransactionManager(ReactiveCouchbaseClientFactory public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { final AtomicReference execResult = new AtomicReference<>(); - couchbaseClientFactory.getCluster().block().transactions().run(ctx -> { - CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(null, true, false, false, true, null, null); + setOptionsFromDefinition(definition); - // Setting ThreadLocal storage - TransactionSynchronizationManager.setActualTransactionActive(true); - TransactionSynchronizationManager.initSynchronization(); - TransactionSynchronizationManager.unbindResourceIfPossible(TransactionAttemptContext.class); - TransactionSynchronizationManager.bindResource(TransactionAttemptContext.class, ctx); + TransactionResult result = couchbaseClientFactory.getCluster().block().transactions().run(ctx -> { + CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(null, true, false, false, true, null, null); + populateTransactionSynchronizationManager(ctx); - ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); - TransactionSynchronizationManager.unbindResourceIfPossible(couchbaseClientFactory.getCluster().block()); - TransactionSynchronizationManager.bindResource(couchbaseClientFactory.getCluster().block(), resourceHolder); + try { + execResult.set(callback.doInTransaction(status)); + } + finally { + TransactionSynchronizationManager.clear(); + } + }, this.options); - try { - execResult.set(callback.doInTransaction(status)); - } - finally { - TransactionSynchronizationManager.clear(); - } - }, this.options); + TransactionSynchronizationManager.clear(); - TransactionSynchronizationManager.clear(); + return execResult.get(); + } + + /** + * @param definition reflects the @Transactional options + */ + private void setOptionsFromDefinition(TransactionDefinition definition) { + if (definition != null) { + if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) { + options = options.timeout(Duration.ofSeconds(definition.getTimeout())); + } + + if (!(definition.getIsolationLevel() == TransactionDefinition.ISOLATION_DEFAULT + || definition.getIsolationLevel() == TransactionDefinition.ISOLATION_READ_COMMITTED)) { + throw new IllegalArgumentException("Couchbase Transactions run at Read Committed isolation - other isolation levels are not supported"); + } + + // readonly is ignored as it is documented as being a hint that won't necessarily cause writes to fail + + // todo gpx what about propagation? + } + + } - return execResult.get(); + // Setting ThreadLocal storage + private void populateTransactionSynchronizationManager(TransactionAttemptContext ctx) { + TransactionSynchronizationManager.setActualTransactionActive(true); + TransactionSynchronizationManager.initSynchronization(); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(AttemptContextReactiveAccessor.getCore(ctx)); + TransactionSynchronizationManager.unbindResourceIfPossible(couchbaseClientFactory.getCluster().block()); + TransactionSynchronizationManager.bindResource(couchbaseClientFactory.getCluster().block(), resourceHolder); } /** @@ -97,18 +122,20 @@ public T execute(TransactionDefinition definition, TransactionCallback ca public TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { TransactionStatus status = new DefaultTransactionStatus( null, true, true, - false, true, false); + false, true, false); return status; } @Override public void commit(TransactionStatus status) throws TransactionException { - LOGGER.debug("NO-OP: Committing Couchbase Transaction with status {}", status); + // todo gpx somewhat nervous that commit/rollback/getTransaction are all left empty but things seem to be working + // anyway... - what are these used for exactly? + LOGGER.debug("NO-OP: Committing Couchbase Transaction with status {}", status); } @Override public void rollback(TransactionStatus status) throws TransactionException { - LOGGER.warn("NO-OP: Rolling back Couchbase Transaction with status {}", status); + LOGGER.warn("NO-OP: Rolling back Couchbase Transaction with status {}", status); } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java index 9a72c7e1e..c5dc26b62 100644 --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java @@ -45,10 +45,9 @@ *

    * Binds a {@link CoreTransactionAttemptContext} from the specified {@link CouchbaseClientFactory} to the thread. *

    - * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link CoreTransactionAttemptContext} - * and enable causal consistency, and also {@link CoreTransactionAttemptContext#startTransaction() start}, - * {@link CoreTransactionAttemptContext#commitTransaction() commit} or - * {@link CoreTransactionAttemptContext#abortTransaction() abort} a transaction. + * {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link CoreTransactionAttemptContext} and enable causal + * consistency, and also {@link CoreTransactionAttemptContext#startTransaction() start}, {@link CoreTransactionAttemptContext#commitTransaction() + * commit} or {@link CoreTransactionAttemptContext#abortTransaction() abort} a transaction. *

    * TODO: Application code is required to retrieve the {@link com.couchbase.client.java.Cluster} ????? via * {@link ?????#getDatabase(CouchbaseClientFactory)} instead of a standard {@link CouchbaseClientFactory#getCluster()} @@ -133,15 +132,14 @@ protected boolean isExistingTransaction(Object transaction) throws TransactionEx protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); - // should ACR already be in TSM? TransactionSynchronizationManager.bindResource(getRequiredDbFactory().getCluster(), - // resourceHolder); - ReactiveCouchbaseResourceHolder resourceHolder = newResourceHolder(getDatabaseFactory(), definition, - TransactionOptions.transactionOptions(), +// should ACR already be in TSM? TransactionSynchronizationManager.bindResource(getRequiredDbFactory().getCluster(), resourceHolder); + ReactiveCouchbaseResourceHolder resourceHolder = newResourceHolder(definition, TransactionOptions.transactionOptions(), null /* ((CouchbaseTransactionDefinition) definition).getAttemptContextReactive()*/); couchbaseTransactionObject.setResourceHolder(resourceHolder); if (logger.isDebugEnabled()) { - logger.debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); + logger + .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); } try { @@ -202,8 +200,7 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio try { doCommit(couchbaseTransactionObject); } catch (Exception ex) { - logger.debug( - "could not commit Couchbase transaction for session " + debugString(couchbaseTransactionObject.getCore())); + logger.debug("could not commit Couchbase transaction for session "+debugString(couchbaseTransactionObject.getCore())); throw new TransactionSystemException(String.format("Could not commit Couchbase transaction for session %s.", debugString(couchbaseTransactionObject.getCore())), ex); } @@ -213,8 +210,9 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio * Customization hook to perform an actual commit of the given transaction.
    * If a commit operation encounters an error, the MongoDB driver throws a {@link CouchbaseException} holding * {@literal error labels}.
    - * By default those labels are ignored, nevertheless one might check for {@link CouchbaseException transient commit - * errors labels} and retry the the commit.
    + * By default those labels are ignored, nevertheless one might check for + * {@link CouchbaseException transient commit errors labels} and retry the the + * commit.
    * *

     	 * int retries = 3;
    @@ -270,8 +268,8 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc
     	protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException {
     
     		CouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status);
    -		throw new TransactionException("need to setRollbackOnly() here") {};
    -		// transactionObject.getRequiredResourceHolder().setRollbackOnly();
    +		throw new TransactionException("need to setRollbackOnly() here"){};
    +		//transactionObject.getRequiredResourceHolder().setRollbackOnly();
     	}
     
     	/*
    @@ -289,7 +287,7 @@ protected void doCleanupAfterCompletion(Object transaction) {
     
     		// Remove the connection holder from the thread.
     		TransactionSynchronizationManager.unbindResourceIfPossible(getRequiredDatabaseFactory().getCluster());
    -		// couchbaseTransactionObject.getRequiredResourceHolder().clear();
    +		//couchbaseTransactionObject.getRequiredResourceHolder().clear();
     
     		if (logger.isDebugEnabled()) {
     			logger.debug(String.format("About to release Core %s after transaction.",
    @@ -446,7 +444,7 @@ void startTransaction(TransactionOptions options) {
     			// if (options != null) {
     			// session.startTransaction(options);
     			// } else {
    -			// core.startTransaction();
    +			//core.startTransaction();
     			// }
     		}
     
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save
    deleted file mode 100644
    index 200463ede..000000000
    --- a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperatorNonReactive.save
    +++ /dev/null
    @@ -1,197 +0,0 @@
    -/*
    - * Copyright 2021 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.transaction;
    -
    -import com.couchbase.transactions.TransactionAttemptContext;
    -import org.springframework.data.couchbase.core.CouchbaseOperations;
    -import org.springframework.transaction.PlatformTransactionManager;
    -import org.springframework.transaction.TransactionManager;
    -import org.springframework.transaction.TransactionStatus;
    -import reactor.core.publisher.Flux;
    -import reactor.core.publisher.Mono;
    -
    -import java.util.HashMap;
    -import java.util.Map;
    -import java.util.function.Function;
    -
    -import org.apache.commons.logging.Log;
    -import org.apache.commons.logging.LogFactory;
    -import org.springframework.data.couchbase.core.CouchbaseTemplate;
    -import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
    -import org.springframework.data.couchbase.repository.DynamicProxyable;
    -import org.springframework.data.couchbase.repository.support.TransactionResultHolder;
    -import org.springframework.transaction.TransactionDefinition;
    -import org.springframework.transaction.TransactionException;
    -import org.springframework.transaction.reactive.TransactionCallback;
    -import org.springframework.transaction.reactive.TransactionContextManager;
    -import org.springframework.transaction.reactive.TransactionalOperator;
    -import org.springframework.util.Assert;
    -
    -import com.couchbase.client.core.error.CouchbaseException;
    -import com.couchbase.transactions.TransactionGetResult;
    -import com.couchbase.transactions.TransactionResult;
    -import com.couchbase.transactions.TransactionsReactive;
    -
    -public class CouchbaseTransactionalOperatorNonReactive implements TransactionalOperator {
    -
    -	// package org.springframework.transaction.reactive;
    -	private static final Log logger = LogFactory.getLog(CouchbaseTransactionalOperatorNonReactive.class);
    -	private final PlatformTransactionManager transactionManager;
    -	private final TransactionDefinition transactionDefinition;
    -
    -	Map getResultMap = new HashMap<>();
    -	private TransactionAttemptContext attemptContext;
    -
    -	public CouchbaseTransactionalOperatorNonReactive(CouchbaseTransactionManager transactionManager) {
    -		this(transactionManager, new CouchbaseTransactionDefinition());
    -	}
    -
    -	public CouchbaseTransactionalOperatorNonReactive(CouchbaseTransactionManager transactionManager,
    -																									 TransactionDefinition transactionDefinition) {
    -		Assert.notNull(transactionManager, "ReactiveTransactionManager must not be null");
    -		Assert.notNull(transactionDefinition, "TransactionDefinition must not be null");
    -		this.transactionManager = transactionManager;
    -		this.transactionDefinition = transactionDefinition;
    -	}
    -
    -	public TransactionResult execute(Function transactionLogic) {
    -		return execute(transactionLogic, true);
    -	}
    -
    -	/**
    -	 * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default
    -	 * PerTransactionConfig.
    -	 */
    -	public TransactionResult execute(Function transactionLogic,
    -			boolean commit) {
    -		return (((CouchbaseTransactionManager) transactionManager).getTransactions().run((ctx) -> {
    -			setAttemptContext(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport
    -			// for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method
    -			// and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder()
    -			transactionLogic.apply(this);
    -		}, null));
    -	}
    -
    -	public TransactionResultHolder transactionResultHolder(Integer key) {
    -		return getResultMap.get(key);
    -	}
    -
    -	public TransactionResultHolder transactionResultHolder(TransactionGetResult result) {
    -		TransactionResultHolder holder = new TransactionResultHolder(result);
    -		getResultMap.put(System.identityHashCode(holder), holder);
    -		return holder;
    -	}
    -
    -	public void setAttemptContext(TransactionAttemptContext attemptContext) {
    -		this.attemptContext = attemptContext;
    -	}
    -
    -	public TransactionAttemptContext getAttemptContext() {
    -		return attemptContext;
    -	}
    -
    -	@Override
    -	public  Flux transactional(Flux flux) {
    -		return execute((it -> flux);
    -	}
    -
    -	@Override
    -	public  Mono transactional(Mono mono) {
    -		return TransactionContextManager.currentContext().flatMap(context -> {
    -			// getCtx()/getAttemptTransActionReactive() has the atr
    -			// atr : transactionalOpterator -> transactionDefinition -> transactionHolder ->
    -			((CouchbaseTransactionDefinition) transactionDefinition).setAttemptContext(getAttemptContext());
    -			TransactionStatus status = this.transactionManager.getTransaction(this.transactionDefinition);
    -			// This is an around advice: Invoke the next interceptor in the chain.
    -			// This will normally result in a target object being invoked.
    -			// Need re-wrapping of ReactiveTransaction until we get hold of the exception
    -			// through usingWhen.
    -			return Mono.just(status)
    -					.flatMap(it -> Mono
    -							.usingWhen(Mono.just(it), ignore -> mono, this.transactionManager::commit, (res, err) -> Mono.empty(),
    -									this.transactionManager::rollback)
    -							.onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex))));
    -		}).contextWrite(TransactionContextManager.getOrCreateContext())
    -				.contextWrite(TransactionContextManager.getOrCreateContextHolder());
    -	}
    -
    -	@Override
    -	public  Flux execute(TransactionCallback action) throws TransactionException {
    -		return TransactionContextManager.currentContext().flatMapMany(context -> {
    -			TransactionStatus status = this.transactionManager.getTransaction(this.transactionDefinition);
    -			// This is an around advice: Invoke the next interceptor in the chain.
    -			// This will normally result in a target object being invoked.
    -			// Need re-wrapping of ReactiveTransaction until we get hold of the exception
    -			// through usingWhen.
    -			return status
    -					.flatMapMany(it -> Flux
    -							.usingWhen(Mono.just(it), action::doInTransaction, this.transactionManager::commit,
    -									(tx, ex) -> Mono.empty(), this.transactionManager::rollback)
    -							.onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex))));
    -		}).contextWrite(TransactionContextManager.getOrCreateContext())
    -				.contextWrite(TransactionContextManager.getOrCreateContextHolder());
    -	}
    -
    -	private void rollbackOnException(TransactionStatus status, Throwable ex) throws TransactionException {
    -		logger.debug("Initiating transaction rollback on application exception", ex);
    -		this.transactionManager.rollback(status);
    -		/*.onErrorMap((ex2) -> {
    -			logger.error("Application exception overridden by rollback exception", ex);
    -			if (ex2 instanceof TransactionSystemException) {
    -				((TransactionSystemException) ex2).initApplicationException(ex);
    -			}
    -			return ex2;
    -		});
    -		 */
    -	}
    -
    -	/*
    -	public TransactionDefinition getTransactionDefinition() {
    -		return transactionDefinition;
    -	}
    -	 */
    -
    -	public TransactionManager getTransactionManager() {
    -		return transactionManager;
    -	}
    -
    -	public CouchbaseTemplate template(CouchbaseTemplate template) {
    -		CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) ((CouchbaseTransactionalOperatorNonReactive) this)
    -				.getTransactionManager());
    -		if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) {
    -			throw new CouchbaseException(
    -					"Template must use the same clientFactory as the transactionManager of the transactionalOperator "
    -							+ template);
    -		}
    -		return template.with(this); // template with a new couchbaseClient with txOperator
    -	}
    -
    -	public > R repository(R repo) {
    -		if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) {
    -			throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo);
    -		}
    -		CouchbaseOperations operations = (CouchbaseOperations) repo.getOperations();
    -		CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) (this).getTransactionManager());
    -
    -		if (operations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) {
    -			throw new CouchbaseException(
    -					"Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + repo);
    -		}
    -		return repo.withTransaction(this); // this returns a new repository proxy with txOperator in its threadLocal
    -		// what if instead we returned a new repo with a new template with the txOperator?
    -	}
    -
    -}
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java
    index f4be7d8f8..31865f1b0 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java
    @@ -69,12 +69,12 @@ public static Mono getDatabase(ReactiveCouchbaseClientFactory
     	 * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
     	 */
     	public static Mono getDatabase(ReactiveCouchbaseClientFactory factory,
    -			SessionSynchronization sessionSynchronization) {
    +													 SessionSynchronization sessionSynchronization) {
     		return doGetCouchbaseCluster(null, factory, sessionSynchronization);
     	}
     
     	public static Mono getTemplate(ReactiveCouchbaseClientFactory factory,
    -			SessionSynchronization sessionSynchronization, CouchbaseConverter converter) {
    +															  SessionSynchronization sessionSynchronization, CouchbaseConverter converter) {
     		return doGetCouchbaseTemplate(null, factory, sessionSynchronization, converter);
     	}
     
    @@ -104,12 +104,12 @@ public static Mono getDatabase(String dbName, ReactiveCouchbas
     	 * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
     	 */
     	public static Mono getCluster(String dbName, ReactiveCouchbaseClientFactory factory,
    -			SessionSynchronization sessionSynchronization) {
    +													SessionSynchronization sessionSynchronization) {
     		return doGetCouchbaseCluster(dbName, factory, sessionSynchronization);
     	}
     
     	private static Mono doGetCouchbaseCluster(@Nullable String dbName,
    -			ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization) {
    +																ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization) {
     
     		Assert.notNull(factory, "DatabaseFactory must not be null!");
     
    @@ -129,8 +129,8 @@ private static Mono doGetCouchbaseCluster(@Nullable String dbN
     	}
     
     	private static Mono doGetCouchbaseTemplate(@Nullable String dbName,
    -			ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization,
    -			CouchbaseConverter converter) {
    +																		  ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization,
    +																		  CouchbaseConverter converter) {
     
     		Assert.notNull(factory, "DatabaseFactory must not be null!");
     
    @@ -166,17 +166,17 @@ private static ReactiveCouchbaseResourceHolder getNonReactiveSession(ReactiveCou
     	}
     
     	private static Mono getCouchbaseClusterOrDefault(@Nullable String dbName,
    -			ReactiveCouchbaseClientFactory factory) {
    +																	   ReactiveCouchbaseClientFactory factory) {
     		return StringUtils.hasText(dbName) ? factory.getCluster() : factory.getCluster();
     	}
     
     	private static Mono getCouchbaseTemplateOrDefault(@Nullable String dbName,
    -			ReactiveCouchbaseClientFactory factory, CouchbaseConverter converter) {
    +																				 ReactiveCouchbaseClientFactory factory, CouchbaseConverter converter) {
     		return Mono.just(new ReactiveCouchbaseTemplate(factory, converter));
     	}
     
     	private static Mono doGetSession(TransactionSynchronizationManager synchronizationManager,
    -			ReactiveCouchbaseClientFactory dbFactory, SessionSynchronization sessionSynchronization) {
    +																	  ReactiveCouchbaseClientFactory dbFactory, SessionSynchronization sessionSynchronization) {
     
     		final ReactiveCouchbaseResourceHolder registeredHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager
     				.getResource(dbFactory.getCluster().block()); // make sure this wasn't saved under the wrong key!!!
    @@ -231,7 +231,7 @@ private static class CouchbaseSessionSynchronization
     		private final ReactiveCouchbaseResourceHolder resourceHolder;
     
     		CouchbaseSessionSynchronization(TransactionSynchronizationManager synchronizationManager,
    -				ReactiveCouchbaseResourceHolder resourceHolder, ReactiveCouchbaseClientFactory dbFactory) {
    +										ReactiveCouchbaseResourceHolder resourceHolder, ReactiveCouchbaseClientFactory dbFactory) {
     
     			super(resourceHolder, dbFactory, synchronizationManager);
     			this.resourceHolder = resourceHolder;
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java
    index 506fb7d26..4e3d09d5c 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java
    @@ -78,7 +78,6 @@ CoreTransactionAttemptContext getRequiredCore() {
     
     	/*
     	 * @return the associated {@link CouchbaseClientFactory}.
    -	
     	ReactiveCouchbaseClientFactory getDatabaseFactory() {
     		return databaseFactory;
     	}
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java
    index 86d2e5535..80665b95f 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java
    @@ -98,6 +98,15 @@ public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databa
     		System.err.println("ReactiveCouchbaseTransactionManager : created");
     	}
     
    +	public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databaseFactory,
    +											   @Nullable Transactions transactions) {
    +		Assert.notNull(databaseFactory, "DatabaseFactory must not be null!");
    +		this.databaseFactory = databaseFactory; // databaseFactory; // should be a clone? TransactionSynchronizationManager
    +		// binds objs to it
    +		this.transactions = transactions;
    +		System.err.println("ReactiveCouchbaseTransactionManager : created Transactions: " + transactions);
    +	}
    +
     	/*
     	 * (non-Javadoc)
     	 * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doGetTransaction(org.springframework.transaction.reactive.TransactionSynchronizationManager)
    @@ -108,7 +117,9 @@ protected Object doGetTransaction(TransactionSynchronizationManager synchronizat
     		// creation of a new ReactiveCouchbaseTransactionObject (i.e. transaction).
     		// with an attempt to get the resourceHolder from the synchronizationManager
     		ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager
    -				.getResource(getRequiredDatabaseFactory().getBlockingCluster());
    +				.getResource(getRequiredDatabaseFactory().getCluster().block());
    +		// TODO ACR from couchbase
    +		// resourceHolder.getSession().setAttemptContextReactive(null);
     		return new ReactiveCouchbaseTransactionObject(resourceHolder);
     	}
     
    @@ -130,7 +141,7 @@ protected boolean isExistingTransaction(Object transaction) throws TransactionEx
     	 */
     	@Override
     	protected Mono doBegin(TransactionSynchronizationManager synchronizationManager, Object transaction,
    -			TransactionDefinition definition) throws TransactionException {
    +								 TransactionDefinition definition) throws TransactionException {
     
     		return Mono.defer(() -> {
     
    @@ -186,7 +197,7 @@ protected Mono doSuspend(TransactionSynchronizationManager synchronizati
     	 */
     	@Override
     	protected Mono doResume(TransactionSynchronizationManager synchronizationManager, @Nullable Object transaction,
    -			Object suspendedResources) {
    +								  Object suspendedResources) {
     		return Mono
     				.fromRunnable(() -> synchronizationManager.bindResource(getRequiredDatabaseFactory(), suspendedResources));
     	}
    @@ -197,7 +208,7 @@ protected Mono doResume(TransactionSynchronizationManager synchronizationM
     	 */
     	@Override
     	protected final Mono doCommit(TransactionSynchronizationManager synchronizationManager,
    -			GenericReactiveTransaction status) throws TransactionException {
    +										GenericReactiveTransaction status) throws TransactionException {
     		return Mono.defer(() -> {
     
     			ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status);
    @@ -225,7 +236,7 @@ protected final Mono doCommit(TransactionSynchronizationManager synchroniz
     	 * @param transactionObject never {@literal null}.
     	 */
     	protected Mono doCommit(TransactionSynchronizationManager synchronizationManager,
    -			ReactiveCouchbaseTransactionObject transactionObject) {
    +								  ReactiveCouchbaseTransactionObject transactionObject) {
     		return transactionObject.commitTransaction();
     	}
     
    @@ -235,7 +246,7 @@ protected Mono doCommit(TransactionSynchronizationManager synchronizationM
     	 */
     	@Override
     	protected Mono doRollback(TransactionSynchronizationManager synchronizationManager,
    -			GenericReactiveTransaction status) {
    +									GenericReactiveTransaction status) {
     
     		return Mono.defer(() -> {
     
    @@ -259,7 +270,7 @@ protected Mono doRollback(TransactionSynchronizationManager synchronizatio
     	 */
     	@Override
     	protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager,
    -			GenericReactiveTransaction status) throws TransactionException {
    +										   GenericReactiveTransaction status) throws TransactionException {
     
     		return Mono.fromRunnable(() -> {
     			ReactiveCouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status);
    @@ -273,7 +284,7 @@ protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchro
     	 */
     	@Override
     	protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager,
    -			Object transaction) {
    +												  Object transaction) {
     
     		Assert.isInstanceOf(ReactiveCouchbaseTransactionObject.class, transaction,
     				() -> String.format("Expected to find a %s but it turned out to be %s.",
    @@ -326,7 +337,7 @@ public void afterPropertiesSet() {
     	}
     
     	private Mono newResourceHolder(TransactionDefinition definition,
    -			TransactionOptions options) {
    +																	TransactionOptions options) {
     
     		ReactiveCouchbaseClientFactory dbFactory = getRequiredDatabaseFactory();
     		// TODO MSR : config should be derived from config that was used for `transactions`
    @@ -417,7 +428,7 @@ final boolean hasResourceHolder() {
     		/**
     		 * Start a XxxxxxXX transaction optionally given {@link TransactionQueryOptions}. todo gp how to expose
     		 * TransactionOptions
    -		 * 
    +		 *
     		 * @param options can be {@literal null}
     		 */
     		void startTransaction() {
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java
    index df90d2b54..ebf1c284b 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java
    @@ -118,27 +118,27 @@ public Object invoke(MethodInvocation methodInvocation) throws Throwable {
         Optional targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType, sessionType);
     
         return !targetMethod.isPresent() ? methodInvocation.proceed()
    -        : ReflectionUtils.invokeMethod(targetMethod.get(), target,
    -        prependSessionToArguments(session, methodInvocation));
    +            : ReflectionUtils.invokeMethod(targetMethod.get(), target,
    +            prependSessionToArguments(session, methodInvocation));
       }
     
       private boolean requiresDecoration(Method method) {
     
         return ClassUtils.isAssignable(databaseType, method.getReturnType())
    -        || ClassUtils.isAssignable(collectionType, method.getReturnType());
    +            || ClassUtils.isAssignable(collectionType, method.getReturnType());
       }
     
       @SuppressWarnings("unchecked")
       protected Object decorate(Object target) {
     
         return ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseDecorator.apply(session, target)
    -        : collectionDecorator.apply(session, target);
    +            : collectionDecorator.apply(session, target);
       }
     
       private static boolean requiresSession(Method method) {
     
         if (method.getParameterCount() == 0
    -        || !ClassUtils.isAssignable(CoreTransactionAttemptContext.class, method.getParameterTypes()[0])) {
    +            || !ClassUtils.isAssignable(CoreTransactionAttemptContext.class, method.getParameterTypes()[0])) {
           return true;
         }
     
    @@ -175,7 +175,7 @@ static class MethodCache {
         Optional lookup(Method method, Class targetClass, Class sessionType) {
     
           return cache.computeIfAbsent(new MethodClassKey(method, targetClass),
    -          val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType)));
    +              val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType)));
         }
     
         @Nullable
    diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java
    index 68243b731..7f3f9c104 100644
    --- a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java
    +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java
    @@ -1,106 +1,157 @@
     package org.springframework.data.couchbase.transaction;
     
    -import static org.springframework.data.couchbase.transaction.CouchbaseTransactionManager.debugString;
    -import static org.springframework.data.couchbase.transaction.CouchbaseTransactionManager.newResourceHolder;
    -
    -import reactor.util.annotation.Nullable;
    -
    -import java.util.function.Consumer;
    -
    -import org.springframework.data.couchbase.CouchbaseClientFactory;
    -import org.springframework.transaction.support.TransactionSynchronizationManager;
    -
    -import com.couchbase.client.core.error.transaction.internal.CoreTransactionFailedException;
    -import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
    -import com.couchbase.client.core.transaction.CoreTransactionResult;
    -import com.couchbase.client.core.transaction.log.CoreTransactionLogger;
    +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
     import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor;
    -import com.couchbase.client.java.transactions.TransactionAttemptContext;
    +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext;
     import com.couchbase.client.java.transactions.TransactionResult;
    -import com.couchbase.client.java.transactions.Transactions;
     import com.couchbase.client.java.transactions.config.TransactionOptions;
    -import com.couchbase.client.java.transactions.error.TransactionFailedException;
    +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory;
    +import org.springframework.transaction.ReactiveTransaction;
    +import org.springframework.transaction.TransactionDefinition;
    +import org.springframework.transaction.reactive.TransactionContextManager;
    +import org.springframework.transaction.reactive.TransactionSynchronizationManager;
    +import reactor.core.publisher.Mono;
    +
    +import java.time.Duration;
    +import java.time.temporal.ChronoUnit;
    +import java.util.function.Function;
     
     // todo gp needed now Transactions has gone?
    -public class TransactionsWrapper /* wraps Transactions */ {
    -	CouchbaseClientFactory couchbaseClientFactory;
    -
    -	public TransactionsWrapper(CouchbaseClientFactory couchbaseClientFactory) {
    -		this.couchbaseClientFactory = couchbaseClientFactory;
    -	}
    -
    -
    -	/**
    -	 * Runs supplied transactional logic until success or failure.
    -	 * 

    - * The supplied transactional logic will be run if necessary multiple times, until either: - *

      - *
    • The transaction successfully commits
    • - *
    • The transactional logic requests an explicit rollback
    • - *
    • The transaction timesout.
    • - *
    • An exception is thrown, either inside the transaction library or by the supplied transaction logic, that cannot - * be handled. - *
    - *

    - * The transaction logic {@link Consumer} is provided an {@link TransactionAttemptContext}, which contains methods - * allowing it to read, mutate, insert and delete documents, as well as commit or rollback the transaction. - *

    - * If the transaction logic performs a commit or rollback it must be the last operation performed. Else a - * {@link com.couchbase.client.java.transactions.error.TransactionFailedException} will be thrown. Similarly, there - * cannot be a commit followed by a rollback, or vice versa - this will also raise a - * {@link CoreTransactionFailedException}. - *

    - * If the transaction logic does not perform an explicit commit or rollback, then a commit will be performed anyway. - * - * @param transactionLogic the application's transaction logic - * @param options the configuration to use for this transaction - * @return there is no need to check the returned {@link CoreTransactionResult}, as success is implied by the lack of - * a thrown exception. It contains information useful only for debugging and logging. - * @throws TransactionFailedException or a derived exception if the transaction fails to commit for any reason, - * possibly after multiple retries. The exception contains further details of the error - */ - - public TransactionResult run(Consumer transactionLogic, - @Nullable TransactionOptions options) { - Consumer newTransactionLogic = (ctx) -> { - try { - CoreTransactionLogger logger = AttemptContextReactiveAccessor.getLogger(ctx); - CoreTransactionAttemptContext atr = AttemptContextReactiveAccessor.getCore(ctx); - - // from CouchbaseTransactionManager - ReactiveCouchbaseResourceHolder resourceHolder = newResourceHolder(couchbaseClientFactory, - /*definition*/ new CouchbaseTransactionDefinition(), TransactionOptions.transactionOptions(), atr); - // couchbaseTransactionObject.setResourceHolder(resourceHolder); - - logger - .debug(String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); - - logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getCore()))); - - TransactionSynchronizationManager.setActualTransactionActive(true); - resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.unbindResourceIfPossible(couchbaseClientFactory.getCluster()); - logger.debug("CouchbaseTransactionManager: " + this); - logger.debug("bindResource: " + couchbaseClientFactory.getCluster() + " value: " + resourceHolder); - TransactionSynchronizationManager.bindResource(couchbaseClientFactory.getCluster(), resourceHolder); - - transactionLogic.accept(ctx); - } finally { - TransactionSynchronizationManager.unbindResource(couchbaseClientFactory.getCluster()); - } - }; - - return AttemptContextReactiveAccessor.run(couchbaseClientFactory.getCluster().transactions(), newTransactionLogic, - options == null ? null : options.build()); - } - - /** - * Runs supplied transactional logic until success or failure. A convenience overload for {@link Transactions#run} - * that provides a default PerTransactionConfig - */ - - public TransactionResult run(Consumer transactionLogic) { - return run(transactionLogic, null); - } +public class TransactionsWrapper { + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; + + public TransactionsWrapper(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) { + this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory; + } + + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default + * PerTransactionConfig. + */ + public Mono reactive(Function> transactionLogic) { + // TODO long duration for debugger + Duration duration = Duration.ofMinutes(20); + System.err.println("tx duration of " + duration); + return run(transactionLogic, TransactionOptions.transactionOptions().timeout(duration)); + } + + public Mono run(Function> transactionLogic) { + return run(transactionLogic,null); + } + public Mono run(Function> transactionLogic, + TransactionOptions perConfig) { + // todo gp this is duplicating a lot of logic from the core loop, and is hopefully not needed.. + // todo mr it binds to with the TransactionSynchronizationManager - which is necessary. + Mono txResult = reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions().run((ctx) -> { + ReactiveCouchbaseResourceHolder resourceHolder = reactiveCouchbaseClientFactory + .getTransactionResources(TransactionOptions.transactionOptions(), AttemptContextReactiveAccessor.getCore(ctx)); + + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + Mono result = transactionLogic.apply(ctx); + result + .onErrorResume(err -> { + AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.toString(), "caught exception '%s' in async, rethrowing", err); + //logElidedStacktrace(ctx, err); + + return Mono.error(new TransactionOperationFailedException(true, true, err, null)); + }) + .thenReturn(ctx); + return result.then(Mono.just(synchronizationManager)); + }); + + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + }); + return txResult; + /* + TransactionsConfig config = TransactionsConfig.create().build(); + + ClusterEnvironment env = ClusterEnvironment.builder().build(); + return Mono.defer(() -> { + MergedTransactionsConfig merged = new MergedTransactionsConfig(config, Optional.of(perConfig)); + + TransactionContext overall = + new TransactionContext(env.requestTracer(), + env.eventBus(), + UUID.randomUUID().toString(), + now(), + Duration.ZERO, + merged); + AtomicReference startTime = new AtomicReference<>(0L); + + Mono ob = Mono.fromCallable(() -> { + String txnId = UUID.randomUUID().toString(); + //overall.LOGGER.info(configDebug(config, perConfig)); + return reactiveCouchbaseClientFactory.getCluster().block().reactive().transactions().createAttemptContext(overall, merged, txnId); + }).flatMap(ctx -> { + + AttemptContextReactiveAccessor.getLogger(ctx).info("starting attempt %d/%s/%s", + overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); + + // begin spring-data-couchbase transaction 1/2 * + ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory + .getSession(ClientSessionOptions.builder().causallyConsistent(true).build(), transactions, null, ctx); + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, + reactiveCouchbaseClientFactory); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + // end spring-data-couchbase transaction 1/2 + Mono result = transactionLogic.apply(ctx); + result + .onErrorResume(err -> { + AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); + logElidedStacktrace(ctx, err); + + return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); + }) + .thenReturn(ctx); + return result.then(Mono.just(synchronizationManager)); + }); + // begin spring-data-couchbase transaction 2/2 + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(Mono.just(ctx)); + // end spring-data-couchbase transaction 2/2 + }).doOnSubscribe(v -> startTime.set(System.nanoTime())) + .doOnNext(v -> AttemptContextReactiveAccessor.getLogger(v).trace(v.attemptId(), "finished attempt %d in %sms", + overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); + + return transactions.reactive().executeTransaction(merged, overall, ob) + .doOnNext(v -> overall.span().finish()) + .doOnError(err -> overall.span().failWith(err)); + }); + + */ + } + + // private void logElidedStacktrace(ReactiveTransactionAttemptContext ctx, Throwable err) { + // transactions.reactive().logElidedStacktrace(ctx, err); + // } + // + // private String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { + // return transactions.reactive().configDebug(config, perConfig); + // } + // + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + + private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, + ReactiveTransaction status, TransactionDefinition definition) { + + // if (status.isNewSynchronization()) { + synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); + synchronizationManager.setCurrentTransactionIsolationLevel( + definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() + : null); + synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); + synchronizationManager.setCurrentTransactionName(definition.getName()); + synchronizationManager.initSynchronization(); + // } + } } diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.save b/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.save deleted file mode 100644 index c32711b60..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/internal/AsyncClientSession.save +++ /dev/null @@ -1,26 +0,0 @@ - -// -// Source code recreated from a .class file by IntelliJ IDEA -// (powered by FernFlower decompiler) -// - -package org.springframework.data.couchbase.transaction.internal; - -import com.couchbase.client.java.transactions.config.TransactionOptions; -import org.springframework.data.couchbase.transaction.ClientSession; - -public interface AsyncClientSession extends ClientSession { - boolean hasActiveTransaction(); - - boolean notifyMessageSent(); - - TransactionOptions getTransactionOptions(); - - void startTransaction(); - - void startTransaction(TransactionOptions var1); - - void commitTransaction(SingleResultCallback var1); - - void abortTransaction(SingleResultCallback var1); -} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save b/src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save deleted file mode 100644 index 717bb662d..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/internal/BaseClientSessionImpl.save +++ /dev/null @@ -1,170 +0,0 @@ - - -// -// Source code recreated from a .class file by IntelliJ IDEA -// (powered by FernFlower decompiler) -// - -package org.springframework.data.couchbase.transaction.internal; - -import com.couchbase.client.java.Scope; -import com.mongodb.ClientSessionOptions; -import com.mongodb.MongoClientException; -import com.mongodb.ServerAddress; -import com.mongodb.assertions.Assertions; -import com.mongodb.internal.binding.ReferenceCounted; -import com.mongodb.lang.Nullable; -import com.mongodb.session.ClientSession; -import com.mongodb.session.ServerSession; -import org.bson.BsonDocument; -import org.bson.BsonTimestamp; -import org.springframework.data.couchbase.transaction.ClientSession; -import org.springframework.data.couchbase.transaction.ClientSessionOptions; -import org.springframework.lang.Nullable; -import reactor.core.publisher.Mono; - -public class BaseClientSessionImpl implements ClientSession { - private static final String CLUSTER_TIME_KEY = "clusterTime"; - private final ServerSessionPool serverSessionPool; - private final ServerSession serverSession; - private final Object originator; - private final ClientSessionOptions options; - private long clusterTime; - private long operationTime; - private long snapshotTimestamp; - private ServerAddress pinnedServerAddress; - private BsonDocument recoveryToken; - private ReferenceCounted transactionContext; - private volatile boolean closed; - - public BaseClientSessionImpl(ServerSessionPool serverSessionPool, Object originator, ClientSessionOptions options) { - this.serverSessionPool = serverSessionPool; - this.serverSession = serverSessionPool.get(); - this.originator = originator; - this.options = options; - this.pinnedServerAddress = null; - this.closed = false; - } - - @Nullable - public ServerAddress getPinnedServerAddress() { - return this.pinnedServerAddress; - } - - public Object getTransactionContext() { - return this.transactionContext; - } - - public void setTransactionContext(ServerAddress address, Object transactionContext) { - Assertions.assertTrue(transactionContext instanceof ReferenceCounted); - this.pinnedServerAddress = address; - this.transactionContext = (ReferenceCounted)transactionContext; - this.transactionContext.retain(); - } - - public void clearTransactionContext() { - this.pinnedServerAddress = null; - if (this.transactionContext != null) { - this.transactionContext.release(); - this.transactionContext = null; - } - - } - - public BsonDocument getRecoveryToken() { - return this.recoveryToken; - } - - public void setRecoveryToken(BsonDocument recoveryToken) { - this.recoveryToken = recoveryToken; - } - - public ClientSessionOptions getOptions() { - return this.options; - } - - public boolean isCausallyConsistent() { - Boolean causallyConsistent = this.options.isCausallyConsistent(); - return causallyConsistent == null ? true : causallyConsistent; - } - - public Object getOriginator() { - return this.originator; - } - - public long getClusterTime() { - return this.clusterTime; - } - - public long getOperationTime() { - return this.operationTime; - } - - @Override - public Mono getScope() { - return null; - } - - public ServerSession getServerSession() { - Assertions.isTrue("open", !this.closed); - return this.serverSession; - } - - public void advanceOperationTime(BsonTimestamp newOperationTime) { - Assertions.isTrue("open", !this.closed); - this.operationTime = this.greaterOf(newOperationTime); - } - - public void advanceClusterTime(BsonDocument newClusterTime) { - Assertions.isTrue("open", !this.closed); - this.clusterTime = this.greaterOf(newClusterTime); - } - - public void setSnapshotTimestamp(BsonTimestamp snapshotTimestamp) { - Assertions.isTrue("open", !this.closed); - if (snapshotTimestamp != null) { - if (this.snapshotTimestamp != null && !snapshotTimestamp.equals(this.snapshotTimestamp)) { - throw new MongoClientException("Snapshot timestamps should not change during the lifetime of the session. Current timestamp is " + this.snapshotTimestamp + ", and attempting to set it to " + snapshotTimestamp); - } - - this.snapshotTimestamp = snapshotTimestamp; - } - - } - - @Nullable - public BsonTimestamp getSnapshotTimestamp() { - Assertions.isTrue("open", !this.closed); - return this.snapshotTimestamp; - } - - private BsonDocument greaterOf(BsonDocument newClusterTime) { - if (newClusterTime == null) { - return this.clusterTime; - } else if (this.clusterTime == null) { - return newClusterTime; - } else { - return newClusterTime.getTimestamp("clusterTime").compareTo(this.clusterTime.getTimestamp("clusterTime")) > 0 ? newClusterTime : this.clusterTime; - } - } - - private long greaterOf(long newOperationTime) { - if (newOperationTime == 0) { - return this.operationTime; - } else if (this.operationTime == 0) { - return newOperationTime; - } else { - return newOperationTime > this.operationTime ? newOperationTime : this.operationTime; - } - } - - public void close() { - if (!this.closed) { - this.closed = true; - this.serverSessionPool.release(this.serverSession); - this.clearTransactionContext(); - } - - } -} - diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save b/src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save deleted file mode 100644 index 368c6efc8..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/internal/ClientSessionPublisherImpl.save +++ /dev/null @@ -1,241 +0,0 @@ - -/* - * Copyright 2008-present MongoDB, Inc. - * - * 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 - * - * http://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.transaction.internal; - -import com.mongodb.ClientSessionOptions; -import com.mongodb.MongoClientException; -import com.mongodb.MongoException; -import com.mongodb.MongoInternalException; -import com.mongodb.ReadConcern; -import com.mongodb.TransactionOptions; -import com.mongodb.WriteConcern; -import com.mongodb.internal.async.SingleResultCallback; -import com.mongodb.internal.async.client.AsyncClientSession; -import com.mongodb.internal.operation.AbortTransactionOperation; -import com.mongodb.internal.operation.AsyncReadOperation; -import com.mongodb.internal.operation.AsyncWriteOperation; -import com.mongodb.internal.operation.CommitTransactionOperation; -import com.mongodb.internal.session.BaseClientSessionImpl; -import com.mongodb.internal.session.ServerSessionPool; -import com.mongodb.reactivestreams.client.ClientSession; -import com.mongodb.reactivestreams.client.MongoClient; -import org.reactivestreams.Publisher; -import org.springframework.data.couchbase.transaction.ClientSession; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; - -import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL; -import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL; -import static com.mongodb.assertions.Assertions.assertTrue; -import static com.mongodb.assertions.Assertions.isTrue; -import static com.mongodb.assertions.Assertions.notNull; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -final class ClientSessionPublisherImpl extends BaseClientSessionImpl implements ClientSession, AsyncClientSession { - - private final OperationExecutor executor; - private TransactionState transactionState = TransactionState.NONE; - private boolean messageSentInCurrentTransaction; - private boolean commitInProgress; - private TransactionOptions transactionOptions; - - ClientSessionPublisherImpl(final ServerSessionPool serverSessionPool, final MongoClient mongoClient, - final ClientSessionOptions options, final OperationExecutor executor) { - super(serverSessionPool, mongoClient, options); - this.executor = executor; - } - - @Override - public boolean hasActiveTransaction() { - return transactionState == TransactionState.IN || (transactionState == TransactionState.COMMITTED && commitInProgress); - } - - @Override - public boolean notifyMessageSent() { - if (hasActiveTransaction()) { - boolean firstMessageInCurrentTransaction = !messageSentInCurrentTransaction; - messageSentInCurrentTransaction = true; - return firstMessageInCurrentTransaction; - } else { - if (transactionState == TransactionState.COMMITTED || transactionState == TransactionState.ABORTED) { - cleanupTransaction(TransactionState.NONE); - } - return false; - } - } - - @Override - public void notifyOperationInitiated(final Object operation) { - assertTrue(operation instanceof AsyncReadOperation || operation instanceof AsyncWriteOperation); - if (!(hasActiveTransaction() || operation instanceof CommitTransactionOperation)) { - assertTrue(getPinnedServerAddress() == null - || (transactionState != TransactionState.ABORTED && transactionState != TransactionState.NONE)); - clearTransactionContext(); - } - } - - @Override - public TransactionOptions getTransactionOptions() { - isTrue("in transaction", transactionState == TransactionState.IN || transactionState == TransactionState.COMMITTED); - return transactionOptions; - } - - @Override - public void startTransaction() { - startTransaction(TransactionOptions.builder().build()); - } - - @Override - public void startTransaction(final TransactionOptions transactionOptions) { - notNull("transactionOptions", transactionOptions); - Boolean snapshot = getOptions().isSnapshot(); - if (snapshot != null && snapshot) { - throw new IllegalArgumentException("Transactions are not supported in snapshot sessions"); - } - if (transactionState == TransactionState.IN) { - throw new IllegalStateException("Transaction already in progress"); - } - if (transactionState == TransactionState.COMMITTED) { - cleanupTransaction(TransactionState.IN); - } else { - transactionState = TransactionState.IN; - } - getServerSession().advanceTransactionNumber(); - this.transactionOptions = TransactionOptions.merge(transactionOptions, getOptions().getDefaultTransactionOptions()); - WriteConcern writeConcern = this.transactionOptions.getWriteConcern(); - if (writeConcern == null) { - throw new MongoInternalException("Invariant violated. Transaction options write concern can not be null"); - } - if (!writeConcern.isAcknowledged()) { - throw new MongoClientException("Transactions do not support unacknowledged write concern"); - } - clearTransactionContext(); - } - - @Override - public void commitTransaction(final SingleResultCallback callback) { - try { - Mono.from(commitTransaction()).subscribe(s -> callback.onResult(s, null), e -> callback.onResult(null, e)); - } catch (Throwable t) { - callback.onResult(null, t); - } - } - - @Override - public void abortTransaction(final SingleResultCallback callback) { - try { - Mono.from(abortTransaction()).subscribe(s -> callback.onResult(s, null), e -> callback.onResult(null, e)); - } catch (Throwable t) { - callback.onResult(null, t); - } - } - - @Override - public AsyncClientSession getWrapped() { - return this; - } - - @Override - public Publisher commitTransaction() { - if (transactionState == TransactionState.ABORTED) { - throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction"); - } - if (transactionState == TransactionState.NONE) { - throw new IllegalStateException("There is no transaction started"); - } - if (!messageSentInCurrentTransaction) { - cleanupTransaction(TransactionState.COMMITTED); - return Mono.create(MonoSink::success); - } else { - ReadConcern readConcern = transactionOptions.getReadConcern(); - if (readConcern == null) { - throw new MongoInternalException("Invariant violated. Transaction options read concern can not be null"); - } - boolean alreadyCommitted = commitInProgress || transactionState == TransactionState.COMMITTED; - commitInProgress = true; - - return executor.execute( - new CommitTransactionOperation(transactionOptions.getWriteConcern(), alreadyCommitted) - .recoveryToken(getRecoveryToken()) - .maxCommitTime(transactionOptions.getMaxCommitTime(MILLISECONDS), MILLISECONDS), - readConcern, this) - .doOnTerminate(() -> { - commitInProgress = false; - transactionState = TransactionState.COMMITTED; - }) - .doOnError(MongoException.class, this::clearTransactionContextOnError); - } - } - - @Override - public Publisher abortTransaction() { - if (transactionState == TransactionState.ABORTED) { - throw new IllegalStateException("Cannot call abortTransaction twice"); - } - if (transactionState == TransactionState.COMMITTED) { - throw new IllegalStateException("Cannot call abortTransaction after calling commitTransaction"); - } - if (transactionState == TransactionState.NONE) { - throw new IllegalStateException("There is no transaction started"); - } - if (!messageSentInCurrentTransaction) { - cleanupTransaction(TransactionState.ABORTED); - return Mono.create(MonoSink::success); - } else { - ReadConcern readConcern = transactionOptions.getReadConcern(); - if (readConcern == null) { - throw new MongoInternalException("Invariant violated. Transaction options read concern can not be null"); - } - return executor.execute( - new AbortTransactionOperation(transactionOptions.getWriteConcern()) - .recoveryToken(getRecoveryToken()), - readConcern, this) - .onErrorResume(Throwable.class, (e) -> Mono.empty()) - .doOnTerminate(() -> { - clearTransactionContext(); - cleanupTransaction(TransactionState.ABORTED); - }); - } - } - - private void clearTransactionContextOnError(final MongoException e) { - if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL) || e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) { - clearTransactionContext(); - } - } - - @Override - public void close() { - if (transactionState == TransactionState.IN) { - Mono.from(abortTransaction()).doOnSuccess(it -> close()).subscribe(); - } else { - super.close(); - } - } - - private void cleanupTransaction(final TransactionState nextState) { - messageSentInCurrentTransaction = false; - transactionOptions = null; - transactionState = nextState; - } - - private enum TransactionState { - NONE, IN, COMMITTED, ABORTED - } -} - diff --git a/src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java b/src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java deleted file mode 100644 index 852687800..000000000 --- a/src/main/java/org/springframework/data/couchbase/transaction/internal/SingleResultCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -// -// Source code recreated from a .class file by IntelliJ IDEA -// (powered by FernFlower decompiler) -// - -package org.springframework.data.couchbase.transaction.internal; - -public interface SingleResultCallback { - void onResult(T var1, Throwable var2); -} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index eb9e44c6c..debe255f4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -161,14 +161,14 @@ public void configureRepositoryOperationsMapping(RepositoryOperationsMapping bas // do not use reactiveCouchbaseTemplate for the name of this method, otherwise the value of that bean // will be used instead of the result of this call (the client factory arg is different) public ReactiveCouchbaseTemplate myReactiveCouchbaseTemplate(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { + MappingCouchbaseConverter mappingCouchbaseConverter) { return new ReactiveCouchbaseTemplate(reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), getDefaultConsistency()); } // do not use couchbaseTemplate for the name of this method, otherwise the value of that been // will be used instead of the result from this call (the client factory arg is different) public CouchbaseTemplate myCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { + MappingCouchbaseConverter mappingCouchbaseConverter) { return new CouchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), getDefaultConsistency()); } @@ -197,7 +197,7 @@ public MappingCouchbaseConverter mappingCouchbaseConverter() { @Override @Bean(name = "mappingCouchbaseConverter") public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext, - CouchbaseCustomConversions couchbaseCustomConversions /* there is a customConversions() method bean */) { + CouchbaseCustomConversions couchbaseCustomConversions /* there is a customConversions() method bean */) { // MappingCouchbaseConverter relies on a SimpleInformationMapper // that has an getAliasFor(info) that just returns getType().getName(). // Our CustomMappingCouchbaseConverter uses a TypeBasedCouchbaseTypeMapper that will diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java new file mode 100644 index 000000000..4aa46ed8e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithoutVersion.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-2021 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.domain; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; +import org.springframework.data.annotation.Version; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.Field; +import org.springframework.data.couchbase.repository.TransactionResult; +import org.springframework.data.domain.Persistable; +import org.springframework.lang.Nullable; + +import java.util.Optional; +import java.util.UUID; + +// todo gpx: lame to C&P the entire Person, but struggling to get a simpler entity working +@Document +public class PersonWithoutVersion extends AbstractEntity implements Persistable { + Optional firstname; + @Nullable Optional lastname; + + @CreatedBy private String creator; + + @LastModifiedBy private String lastModifiedBy; + + @LastModifiedDate private long lastModification; + + @CreatedDate private long creationDate; + + @Nullable @Field("nickname") private String middlename; + @Nullable @Field(name = "prefix") private String salutation; + + private Address address; + + // Required for use in transactions + @TransactionResult private Integer txResultHolder; + @Transient private boolean isNew; + + + public PersonWithoutVersion() {} + + public PersonWithoutVersion(String firstname, String lastname) { + this(); + setFirstname(firstname); + setLastname(lastname); + setMiddlename("Nick"); + isNew(true); + } + + public PersonWithoutVersion(int id, String firstname, String lastname) { + this(firstname, lastname); + setId(new UUID(id, id)); + } + + public PersonWithoutVersion(UUID id, String firstname, String lastname) { + this(firstname, lastname); + setId(id); + } + + static String optional(String name, Optional obj) { + if (obj != null) { + if (obj.isPresent()) { + return (" " + name + ": '" + obj.get() + "'"); + } else { + return " " + name + ": null"; + } + } + return ""; + } + + public String getFirstname() { + return firstname.get(); + } + + public void setFirstname(String firstname) { + this.firstname = firstname == null ? null : (Optional.ofNullable(firstname.equals("") ? null : firstname)); + } + + public void setFirstname(Optional firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname.get(); + } + + public void setLastname(String lastname) { + this.lastname = lastname == null ? null : (Optional.ofNullable(lastname.equals("") ? null : lastname)); + } + + public void setLastname(Optional lastname) { + this.lastname = lastname; + } + + public String getMiddlename() { + return middlename; + } + + public String getSalutation() { + return salutation; + } + + public void setMiddlename(String middlename) { + this.middlename = middlename; + } + + public void setSalutation(String salutation) { + this.salutation = salutation; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Person : {\n"); + sb.append(" id : " + getId()); + sb.append(optional(", firstname", firstname)); + sb.append(optional(", lastname", lastname)); + if (middlename != null) + sb.append(", middlename : '" + middlename + "'"); + if (creator != null) { + sb.append(", creator : " + creator); + } + if (creationDate != 0) { + sb.append(", creationDate : " + creationDate); + } + if (lastModifiedBy != null) { + sb.append(", lastModifiedBy : " + lastModifiedBy); + } + if (lastModification != 0) { + sb.append(", lastModification : " + lastModification); + } + if (getAddress() != null) { + sb.append(", address : " + getAddress().toString()); + } + sb.append("\n}"); + return sb.toString(); + } + + public PersonWithoutVersion withFirstName(String firstName) { + PersonWithoutVersion p = new PersonWithoutVersion(this.getId(), firstName, this.getLastname()); + p.txResultHolder = this.txResultHolder; + return p; + } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + + PersonWithoutVersion that = (PersonWithoutVersion) obj; + return this.getId().equals(that.getId()) && this.getFirstname().equals(that.getFirstname()) + && this.getLastname().equals(that.getLastname()) && this.getMiddlename().equals(that.getMiddlename()); + } + + @Override + public boolean isNew() { + return isNew; + } + + public void isNew(boolean isNew){ + this.isNew = isNew; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index f59e85e41..f648063d2 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -881,7 +881,7 @@ void threadSafeStringParametersTest() throws Exception { } @Test - // DATACOUCH-650 + // DATACOUCH-650 void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java index 8a345742b..db8b35d40 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -24,6 +24,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.config.TransactionOptions; import lombok.Data; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -57,6 +60,8 @@ import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.ReactivePersonRepository; import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; import org.springframework.data.couchbase.transaction.ReactiveTransactionsWrapper; import org.springframework.data.couchbase.transaction.TransactionsWrapper; @@ -69,15 +74,23 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.transaction.support.DefaultTransactionDefinition; import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.core.error.transaction.RetryTransactionException; import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; import com.couchbase.client.java.Collection; import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.Transactions; import com.couchbase.client.java.transactions.error.TransactionFailedException; /** @@ -280,6 +293,11 @@ public void insertTwicePersonCBTransactionsRxTmplRollback() { assertNull(pFound, "insert should have been rolled back"); } + /** + * This test has the bare minimum for reactive transactions. Create the ClientSession that holds the ctx and put it in + * a resourceHolder and binds it to the currentContext. The retries are handled by couchbase-transactions - which + * creates a new ctx and re-runs the lambda. This is essentially what TransactionWrapper does. + */ @Test public void wrapperReplaceWithCasConflictResolvedViaRetry() { Person person = new Person(1, "Walter", "White"); @@ -716,7 +734,7 @@ static class PersonService { final ReactiveCouchbaseTransactionManager managerRx; public PersonService(CouchbaseOperations ops, CouchbaseSimpleCallbackTransactionManager mgr, - ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { personOperations = ops; manager = mgr; System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java index cf6c934c8..b92bd331b 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -587,26 +587,26 @@ PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionMana } */ - } - - @Data - // @AllArgsConstructor - static class EventLog { - public EventLog() {} - - public EventLog(ObjectId oid, String action) { - this.id = oid.toString(); - this.action = action; - } - - public EventLog(String id, String action) { - this.id = id; - this.action = action; - } - - String id; - String action; - @Version - Long version; - } + } + + @Data + // @AllArgsConstructor + static class EventLog { + public EventLog() {} + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + public EventLog(String id, String action) { + this.id = id; + this.action = action; + } + + String id; + String action; + @Version + Long version; + } } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java index dacfab844..1dd8e795d 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -130,7 +130,7 @@ public void replacePersonRbTemplate() { Person person = new Person(1, "Walter", "White"); remove(rxCBTmpl, cName, person.getId().toString()); rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); -sleepMs(1000); + sleepMs(1000); CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); Mono result = txOperator .reactive((ctx) -> ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) @@ -374,7 +374,7 @@ public Mono savePerson(Person person) { .as(transactionalOperator::transactional); } - void remove(Collection col, String id) { + void remove(Collection col, String id) { remove(col.reactive(), id); } diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java index 6e0862d04..72d91e07c 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java @@ -140,7 +140,7 @@ public void verifyDbState() { assertionList.forEach(it -> { boolean isPresent = template.findById(Assassin.class).one(it.getId().toString()) != null; // (Filters.eq("_id", - // it.getId())) != 0; + // it.getId())) != 0; assertThat(isPresent).isEqualTo(it.shouldBePresent()) .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalIntegrationTests.java new file mode 100644 index 000000000..cee3a1fb0 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalIntegrationTests.java @@ -0,0 +1,497 @@ +/* + * Copyright 2012-2021 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.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithoutVersion; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +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.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +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.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for @Transactional. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +public class CouchbaseTransactionalIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + /* DO NOT @Autowired - it will result in no @Transactional annotation behavior */ PersonService personService; + @Autowired CouchbaseTemplate operations; + + static GenericApplicationContext context; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + context = new AnnotationConfigApplicationContext(Config.class, PersonService.class); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + personService = context.getBean(PersonService.class); // getting it via autowired results in no @Transactional + // Skip this as we just one to track TransactionContext + operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); // doesn't work??? + List p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + + Person walterWhite = new Person(1, "Walter", "White"); + try { + couchbaseClientFactory.getBucket().defaultCollection().remove(walterWhite.getId().toString()); + } catch (Exception ex) { + // System.err.println(ex); + } + } + + @DisplayName("A basic golden path insert should succeed") + @Test + public void committedInsert() { + AtomicInteger tryCount = new AtomicInteger(0); + + Person inserted = personService.doInTransaction(tryCount, (ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.insertById(Person.class).one(person); + return person; + }); + + Person fetched = operations.findById(Person.class).one(inserted.getId().toString()); + assertEquals("Walter", fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path replace should succeed") + @Test + public void committedReplace() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + + personService.fetchAndReplace(person.getId().toString(), tryCount, (p) -> { + p.setFirstname("changed"); + return p; + }); + + Person fetched = operations.findById(Person.class).one(person.getId().toString()); + assertEquals("changed", fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("A basic golden path remove should succeed") + @Test + public void committedRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + + personService.fetchAndRemove(person.getId().toString(), tryCount); + + Person fetched = operations.findById(Person.class).one(person.getId().toString()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing an insert then rolling back") + @Test + public void rollbackInsert() { + AtomicInteger tryCount = new AtomicInteger(0); + AtomicReference id = new AtomicReference<>(); + + try { + personService.doInTransaction(tryCount, (ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.insertById(Person.class).one(person); + id.set(person.getId().toString()); + throw new SimulateFailureException(); + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof SimulateFailureException); + } + + Person fetched = operations.findById(Person.class).one(id.get()); + assertNull(fetched); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a replace then rolling back") + @Test + public void rollbackReplace() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + + try { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).one(person.getId().toString()); + p.setFirstname("changed"); + ops.replaceById(Person.class).one(p); + throw new SimulateFailureException(); + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof SimulateFailureException); + } + + Person fetched = operations.findById(Person.class).one(person.getId().toString()); + assertEquals("Walter", fetched.getFirstname()); + assertEquals(1, tryCount.get()); + } + + @DisplayName("Basic test of doing a remove then rolling back") + @Test + public void rollbackRemove() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + + try { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).one(person.getId().toString()); + ops.removeById(Person.class).oneEntity(p); + throw new SimulateFailureException(); + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof SimulateFailureException); + } + + Person fetched = operations.findById(Person.class).one(person.getId().toString()); + assertNotNull(fetched); + assertEquals(1, tryCount.get()); + } + + @Test + public void shouldRollbackAfterException() { + try { + personService.insertThenThrow(); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof SimulateFailureException); + } + + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void commitShouldPersistTxEntries() { + Person p = new Person(null, "Walter", "White"); + Person s = personService.declarativeSavePerson(p); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Disabled("because hanging - requires JCBC-1955 fix") + @Test + public void concurrentTxns() { + Runnable r = () -> { + Thread t = Thread.currentThread(); + System.out.printf("Started thread %d %s%n", t.getId(), t.getName()); + Person p = new Person(null, "Walter", "White"); + Person s = personService.declarativeSavePersonWithThread(p, t); + System.out.printf("Finished thread %d %s%n", t.getId(), t.getName()); + }; + List threads = new ArrayList<>(); + for (int i = 0; i < 99; i ++) { + Thread t = new Thread(r); + t.start(); + threads.add(t); + } + threads.forEach(t -> { + try { + System.out.printf("Waiting for thread %d %s%n", t.getId(), t.getName()); + t.join(); + System.out.printf("Finished waiting for thread %d %s%n", t.getId(), t.getName()); + } catch (InterruptedException e) { + fail(); + } + }); + } + + // 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.") + @Test + public void replacePerson() { + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + + System.out.printf("insert CAS: %s%n", person.getVersion()); + + Person refetched = operations.findById(Person.class).one(person.getId().toString()); + operations.replaceById(Person.class).one(refetched); + + System.out.printf("replace CAS: %s%n", refetched.getVersion()); + + assertNotEquals(person.getVersion(), refetched.getVersion()); + + AtomicInteger tryCount = new AtomicInteger(0); + // todo gpx this is raising incorrect error: + // com.couchbase.client.core.retry.reactor.RetryExhaustedException: com.couchbase.client.core.error.transaction.RetryTransactionException: User request to retry transaction + personService.replace(person, tryCount); + } + + + @DisplayName("Entity must have CAS field during replace") + @Test + public void replaceEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(1, "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).one(person); + try { + personService.replaceEntityWithoutVersion(person.getId().toString()); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalArgumentException); + } + } + + @DisplayName("Entity must have non-zero CAS during replace") + @Test + public void replaceEntityWithCasZero() { + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + + // switchedPerson here will have CAS=0, which will fail + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(0); + + try { + personService.replacePerson(switchedPerson, tryCount); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalArgumentException); + } + } + + @DisplayName("Entity must have CAS field during remove") + @Test + public void removeEntityWithoutCas() { + PersonWithoutVersion person = new PersonWithoutVersion(1, "Walter", "White"); + operations.insertById(PersonWithoutVersion.class).one(person); + try { + personService.removeEntityWithoutVersion(person.getId().toString()); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalArgumentException); + } + } + + @DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS") + @Test + public void removeEntityById() { + AtomicInteger tryCount = new AtomicInteger(0); + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + + try { + personService.doInTransaction(tryCount, (ops) -> { + Person p = ops.findById(Person.class).one(person.getId().toString()); + ops.removeById(Person.class).one(p.getId().toString()); + return p; + }); + fail(); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalArgumentException); + } + } + + @Service + @Component + @EnableTransactionManagement + static + class PersonService { + final CouchbaseOperations personOperations; + final ReactiveCouchbaseOperations personOperationsRx; + + public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx) { + personOperations = ops; + personOperationsRx = opsRx; + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out.println(String.format("Thread %d %s", Thread.currentThread().getId(), Thread.currentThread().getName())); + Person ret = personOperations.insertById(Person.class).one(person); + System.out.println(String.format("Thread %d (was %d) %s", Thread.currentThread().getId(), currentThreadId, Thread.currentThread().getName())); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeSavePersonWithThread(Person person, Thread thread) { + assertInAnnotationTransaction(true); + long currentThreadId = Thread.currentThread().getId(); + System.out.printf("Thread %d %s, started from %d %s%n", Thread.currentThread().getId(), Thread.currentThread().getName(), thread.getId(), thread.getName()); + Person ret = personOperations.insertById(Person.class).one(person); + System.out.printf("Thread %d (was %d) %s, started from %d %s%n", Thread.currentThread().getId(), currentThreadId, Thread.currentThread().getName(), thread.getId(), thread.getName()); + if (currentThreadId != Thread.currentThread().getId()) { + throw new IllegalStateException(); + } + return ret; + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public void insertThenThrow() { + Person person = new Person(null, "Walter", "White"); + assertInAnnotationTransaction(true); + personOperations.insertById(Person.class).one(person); + SimulateFailureException.throwEx(); + } + + @Autowired CouchbaseSimpleCallbackTransactionManager callbackTm; + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person replacePerson(Person person, AtomicInteger tryCount) { + tryCount.incrementAndGet(); + // Note that passing in a Person and replace it in this way, is not supported + return personOperations.replaceById(Person.class).one(person); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public void replaceEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).one(id); + personOperations.replaceById(PersonWithoutVersion.class).one(fetched); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public void removeEntityWithoutVersion(String id) { + PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).one(id); + personOperations.removeById(PersonWithoutVersion.class).oneEntity(fetched); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeFindReplaceTwicePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); +// System.err.println("declarativeFindReplacePersonCallback cluster : " +// + callbackTm.template().getCouchbaseClientFactory().getCluster().block()); +// System.err.println("declarativeFindReplacePersonCallback resourceHolder : " +// + org.springframework.transaction.support.TransactionSynchronizationManager +// .getResource(callbackTm.template().getCouchbaseClientFactory().getCluster().block())); + Person p = personOperations.findById(Person.class).one(person.getId().toString()); + Person pUpdated = personOperations.replaceById(Person.class).one(p); + return personOperations.replaceById(Person.class).one(pUpdated); + } + + + // todo gpx how do we make COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER the default so user only has to specify @Transactional, without the transactionManager? + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person replace(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + return personOperations.replaceById(Person.class).one(person); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person fetchAndReplace(String id, AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).one(id); + Person modified = callback.apply(p); + return personOperations.replaceById(Person.class).one(modified); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public T doInTransaction(AtomicInteger tryCount, Function callback) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public void fetchAndRemove(String id, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + tryCount.incrementAndGet(); + Person p = personOperations.findById(Person.class).one(id); + personOperations.removeById(Person.class).oneEntity(p); + } + } + + static void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName().startsWith("org.springframework.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException( + "in transaction = " + (!inTransaction) + " but expected in annotation transaction = " + inTransaction); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java new file mode 100644 index 000000000..fc0fcda2a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalNonAllowableOperationsIntegrationTests.java @@ -0,0 +1,148 @@ +/* + * 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.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.domain.Person; +import org.springframework.data.couchbase.util.Capabilities; +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.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for @Transactional methods, where operations that aren't supported in a transaction are being used. + * They should be prevented at runtime. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +public class CouchbaseTransactionalNonAllowableOperationsIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + PersonService personService; + 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); + } + } + + void test(Consumer r) { + AtomicInteger tryCount = new AtomicInteger(0); + + try { + personService.doInTransaction(tryCount, (ops) -> { + r.accept(ops); + return null; + }); + fail("Transaction should not succeed"); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalArgumentException); + } + + assertEquals(1, tryCount.get()); + } + + @DisplayName("Using existsById() in a transaction is rejected at runtime") + @Test + public void existsById() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.existsById(Person.class).one(person.getId().toString()); + }); + } + + @DisplayName("Using findByAnalytics() in a transaction is rejected at runtime") + @Test + public void findByAnalytics() { + test((ops) -> { + ops.findByAnalytics(Person.class).one(); + }); + } + + @DisplayName("Using findFromReplicasById() in a transaction is rejected at runtime") + @Test + public void findFromReplicasById() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.findFromReplicasById(Person.class).any(person.getId().toString()); + }); + } + + @DisplayName("Using upsertById() in a transaction is rejected at runtime") + @Test + public void upsertById() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.upsertById(Person.class).one(person); + }); + } + + @Service + @Component + @EnableTransactionManagement + static + class PersonService { + final CouchbaseOperations personOperations; + + public PersonService(CouchbaseOperations ops) { + personOperations = ops; + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public T doInTransaction(AtomicInteger tryCount, Function callback) { + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java new file mode 100644 index 000000000..62941e8ce --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalOptionsIntegrationTests.java @@ -0,0 +1,147 @@ +/* + * 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.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.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.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for @Transactional methods, setting all the various options allowed by @Transactional. + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +public class CouchbaseTransactionalOptionsIntegrationTests extends JavaIntegrationTests { + + @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); + } + } + + @DisplayName("@Transactional(timeout = 2) will timeout at around 2 seconds") + @Test + public void timeout() { + long start = System.nanoTime(); + Person person = new Person(1, "Walter", "White"); + operations.insertById(Person.class).one(person); + try { + personService.timeout(person.getId().toString()); + fail(); + } + catch (TransactionFailedException err) { + } + Duration timeTaken = Duration.ofNanos(System.nanoTime() - start); + assertTrue(timeTaken.toMillis() >= 2000); + assertTrue(timeTaken.toMillis() < 10_000); // Default transaction timeout is 15s + } + + @DisplayName("@Transactional(isolation = Isolation.ANYTHING_BUT_READ_COMMITTED) will fail") + @Test + public void unsupportedIsolation() { + try { + personService.unsupportedIsolation(); + fail(); + } + catch (IllegalArgumentException err) { + } + } + + @DisplayName("@Transactional(isolation = Isolation.READ_COMMITTED) will succeed") + @Test + public void supportedIsolation() { + personService.supportedIsolation(); + } + + @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 T doInTransaction(AtomicInteger tryCount, Function callback) { + tryCount.incrementAndGet(); + return callback.apply(ops); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, timeout = 2) + public void timeout(String id) { + while (true) { + Person p = ops.findById(Person.class).one(id); + ops.replaceById(Person.class).one(p); + } + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, isolation = Isolation.REPEATABLE_READ) + public void unsupportedIsolation() { + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER, isolation = Isolation.READ_COMMITTED) + public void supportedIsolation() { + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java new file mode 100644 index 000000000..3aadd409a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java @@ -0,0 +1,193 @@ +/* + * 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.kv.InsertOptions; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.kv.ReplaceOptions; +import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.transactions.error.TransactionFailedException; +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.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.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.util.Capabilities; +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.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for @Transactional methods, where parameters/options are being set that aren't support in a transaction. + * These will be rejected at runtime. + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) +public class CouchbaseTransactionalUnsettableParametersIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + PersonService personService; + 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); // getting it via autowired results in no @Transactional + + Person walterWhite = new Person(1, "Walter", "White"); + try { + couchbaseClientFactory.getBucket().defaultCollection().remove(walterWhite.getId().toString()); + } catch (Exception ex) { + // System.err.println(ex); + } + } + + void test(Consumer r) { + AtomicInteger tryCount = new AtomicInteger(0); + + try { + personService.doInTransaction(tryCount, (ops) -> { + r.accept(ops); + return null; + }); + fail("Transaction should not succeed"); + } + catch (TransactionFailedException err) { + assertTrue(err.getCause() instanceof IllegalArgumentException); + } + + assertEquals(1, tryCount.get()); + } + + @DisplayName("Using insertById().withDurability - the PersistTo overload - in a transaction is rejected at runtime") + @Test + public void insertWithDurability() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.insertById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).one(person); + }); + } + + @DisplayName("Using insertById().withExpiry in a transaction is rejected at runtime") + @Test + public void insertWithExpiry() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.insertById(Person.class).withExpiry(Duration.ofSeconds(3)).one(person); + }); + } + + @DisplayName("Using insertById().withOptions in a transaction is rejected at runtime") + @Test + public void insertWithOptions() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.insertById(Person.class).withOptions(InsertOptions.insertOptions()).one(person); + }); + } + + @DisplayName("Using replaceById().withDurability - the PersistTo overload - in a transaction is rejected at runtime") + @Test + public void replaceWithDurability() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.replaceById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).one(person); + }); + } + + @DisplayName("Using replaceById().withExpiry in a transaction is rejected at runtime") + @Test + public void replaceWithExpiry() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.replaceById(Person.class).withExpiry(Duration.ofSeconds(3)).one(person); + }); + } + + @DisplayName("Using replaceById().withOptions in a transaction is rejected at runtime") + @Test + public void replaceWithOptions() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.replaceById(Person.class).withOptions(ReplaceOptions.replaceOptions()).one(person); + }); + } + + @DisplayName("Using removeById().withDurability - the PersistTo overload - in a transaction is rejected at runtime") + @Test + public void removeWithDurability() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.removeById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).oneEntity(person); + }); + } + + @DisplayName("Using removeById().withOptions in a transaction is rejected at runtime") + @Test + public void removeWithOptions() { + test((ops) -> { + Person person = new Person(1, "Walter", "White"); + ops.removeById(Person.class).withOptions(RemoveOptions.removeOptions()).oneEntity(person); + }); + } + + @Service + @Component + @EnableTransactionManagement + static + class PersonService { + final CouchbaseOperations personOperations; + + public PersonService(CouchbaseOperations ops) { + personOperations = ops; + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public T doInTransaction(AtomicInteger tryCount, Function callback) { + tryCount.incrementAndGet(); + return callback.apply(personOperations); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index 975170cca..e9149bf92 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -146,7 +146,7 @@ protected static void createPrimaryIndex(final Cluster cluster, final String buc } public static void setupScopeCollection(Cluster cluster, String scopeName, String collectionName, - CollectionManager collectionManager) { + CollectionManager collectionManager) { // Create the scope.collection (borrowed from CollectionManagerIntegrationTest ) ScopeSpec scopeSpec = ScopeSpec.create(scopeName); CollectionSpec collSpec = CollectionSpec.create(collectionName, scopeName); @@ -278,7 +278,7 @@ public static boolean scopeExists(CollectionManager mgr, String scopeName) { } public static CompletableFuture createPrimaryIndex(Cluster cluster, String bucketName, String scopeName, - String collectionName) { + String collectionName) { CreatePrimaryQueryIndexOptions options = CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions(); options.timeout(Duration.ofSeconds(300)); options.ignoreIfExists(true); @@ -303,14 +303,14 @@ public static CompletableFuture createPrimaryIndex(Cluster cluster, String private static CompletableFuture exec(Cluster cluster, /*AsyncQueryIndexManager.QueryType queryType*/ boolean queryType, CharSequence statement, - Map with, CommonOptions.BuiltCommonOptions options) { + Map with, CommonOptions.BuiltCommonOptions options) { return with.isEmpty() ? exec(cluster, queryType, statement, options) : exec(cluster, queryType, statement + " WITH " + Mapper.encodeAsString(with), options); } private static CompletableFuture exec(Cluster cluster, /*AsyncQueryIndexManager.QueryType queryType,*/ boolean queryType, CharSequence statement, - CommonOptions.BuiltCommonOptions options) { + CommonOptions.BuiltCommonOptions options) { QueryOptions queryOpts = toQueryOptions(options).readonly(queryType /*requireNonNull(queryType) == READ_ONLY*/); return cluster.async().query(statement.toString(), queryOpts).exceptionally(t -> { @@ -342,7 +342,7 @@ private static RuntimeException translateException(Throwable t) { } public static void createFtsCollectionIndex(Cluster cluster, String indexName, String bucketName, String scopeName, - String collectionName) { + String collectionName) { SearchIndex searchIndex = new SearchIndex(indexName, bucketName); if (scopeName != null) { // searchIndex = searchIndex.forScopeCollection(scopeName, collectionName); @@ -386,14 +386,14 @@ public static Throwable assertThrowsOneOf(Executable executable, Class... exp executable.execute(); } catch (Throwable actualException) { - for(Class expectedType:expectedTypes){ - if(actualException.getClass().isAssignableFrom( expectedType)){ - return actualException; - } + for(Class expectedType:expectedTypes){ + if(actualException.getClass().isAssignableFrom( expectedType)){ + return actualException; } - UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); - String message = "Unexpected exception type thrown "+actualException.getClass(); - throw new AssertionFailedError(message, actualException); + } + UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + String message = "Unexpected exception type thrown "+actualException.getClass(); + throw new AssertionFailedError(message, actualException); } String message ="Expected "+expectedTypes+" to be thrown, but nothing was thrown."; diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java index 9e30569a7..6c6fea9ae 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Util.java +++ b/src/test/java/org/springframework/data/couchbase/util/Util.java @@ -51,17 +51,17 @@ public static void waitUntilCondition(final BooleanSupplier supplier, Duration a public static void waitUntilThrows(final Class clazz, final Supplier supplier) { with() - .pollInterval(Duration.ofMillis(1)) - .await() - .atMost(Duration.ofMinutes(1)) - .until(() -> { - try { - supplier.get(); - } catch (final Exception ex) { - return ex.getClass().isAssignableFrom(clazz); - } - return false; - }); + .pollInterval(Duration.ofMillis(1)) + .await() + .atMost(Duration.ofMinutes(1)) + .until(() -> { + try { + supplier.get(); + } catch (final Exception ex) { + return ex.getClass().isAssignableFrom(clazz); + } + return false; + }); } /** @@ -104,7 +104,7 @@ public static void assertInAnnotationTransaction(boolean inTransaction) { StackTraceElement[] stack = Thread.currentThread().getStackTrace(); for (StackTraceElement ste : stack) { if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") - || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { + || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { if (inTransaction) { return; } @@ -114,7 +114,7 @@ public static void assertInAnnotationTransaction(boolean inTransaction) { return; } throw new RuntimeException( - "in-annotation-transaction = " + (!inTransaction) + " but expected in-annotation-transaction = " + inTransaction); + "in-annotation-transaction = " + (!inTransaction) + " but expected in-annotation-transaction = " + inTransaction); } }