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); } }