diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100755 index 01e679973..000000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/pom.xml b/pom.xml index 6aac4a641..2a4ec9bca 100644 --- a/pom.xml +++ b/pom.xml @@ -18,8 +18,8 @@ - 3.2.5 - 3.2.5 + 3.3.0 + 3.3.0 2.7.0-SNAPSHOT spring.data.couchbase @@ -37,6 +37,7 @@ + org.springframework spring-context-support @@ -180,7 +181,14 @@ com.squareup.okhttp3 okhttp - 4.4.0 + 4.8.1 + test + + + + com.squareup.okhttp3 + okhttp-tls + 4.8.1 test @@ -212,6 +220,18 @@ 4.0.3 test + + org.testcontainers + testcontainers + + + + ch.qos.logback + logback-classic + 1.2.5 + compile + + @@ -230,10 +250,6 @@ false - - jitpack.io - https://jitpack.io - @@ -265,6 +281,7 @@ org.apache.maven.plugins maven-failsafe-plugin + false **/*IntegrationTest.java **/*IntegrationTests.java diff --git a/src/main/java/com/couchbase/client/java/Cluster.java b/src/main/java/com/couchbase/client/java/Cluster.java new file mode 100644 index 000000000..23c588033 --- /dev/null +++ b/src/main/java/com/couchbase/client/java/Cluster.java @@ -0,0 +1,589 @@ +/* + * Copyright (c) 2018 Couchbase, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.couchbase.client.java; + +import com.couchbase.client.core.Core; +import com.couchbase.client.core.diagnostics.ClusterState; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.diagnostics.PingResult; +import com.couchbase.client.core.env.Authenticator; +import com.couchbase.client.core.env.PasswordAuthenticator; +import com.couchbase.client.core.env.SeedNode; +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.core.error.TimeoutException; +import com.couchbase.client.core.msg.search.SearchRequest; +import com.couchbase.client.java.analytics.AnalyticsOptions; +import com.couchbase.client.java.analytics.AnalyticsResult; +import com.couchbase.client.java.diagnostics.DiagnosticsOptions; +import com.couchbase.client.java.diagnostics.PingOptions; +import com.couchbase.client.java.diagnostics.WaitUntilReadyOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.manager.analytics.AnalyticsIndexManager; +import com.couchbase.client.java.manager.bucket.BucketManager; +import com.couchbase.client.java.manager.eventing.EventingFunctionManager; +import com.couchbase.client.java.manager.query.QueryIndexManager; +import com.couchbase.client.java.manager.search.SearchIndexManager; +import com.couchbase.client.java.manager.user.UserManager; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.search.SearchOptions; +import com.couchbase.client.java.search.SearchQuery; +import com.couchbase.client.java.search.result.SearchResult; +import com.couchbase.client.java.transactions.Transactions; + +import java.time.Duration; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import static com.couchbase.client.core.util.Validators.notNull; +import static com.couchbase.client.core.util.Validators.notNullOrEmpty; +import static com.couchbase.client.java.AsyncCluster.extractClusterEnvironment; +import static com.couchbase.client.java.AsyncCluster.seedNodesFromConnectionString; +import static com.couchbase.client.java.AsyncUtils.block; +import static com.couchbase.client.java.ClusterOptions.clusterOptions; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_ANALYTICS_OPTIONS; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_DIAGNOSTICS_OPTIONS; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_QUERY_OPTIONS; +import static com.couchbase.client.java.ReactiveCluster.DEFAULT_SEARCH_OPTIONS; + +/** + * The {@link Cluster} is the main entry point when connecting to a Couchbase cluster. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * This method will wait until either the cluster state is "online" by default, or the timeout is reached. Since the + * SDK is bootstrapping lazily, this method allows to eagerly check during bootstrap if all of the services are online + * and usable before moving on. You can tune the properties through {@link WaitUntilReadyOptions}. + * + * @param timeout the maximum time to wait until readiness. + * @param options the options to customize the readiness waiting. + */ + public void waitUntilReady(final Duration timeout, final WaitUntilReadyOptions options) { + block(asyncCluster.waitUntilReady(timeout, options)); + } + + /** + * Allows access to transactions. + * + * @return the {@link Transactions} interface. + */ + @Stability.Uncommitted + public Transactions transactions() { + return new Transactions(core(), environment().jsonSerializer()); + } +} + diff --git a/src/main/java/com/couchbase/client/java/ClusterInterface.java b/src/main/java/com/couchbase/client/java/ClusterInterface.java new file mode 100644 index 000000000..872a6efdf --- /dev/null +++ b/src/main/java/com/couchbase/client/java/ClusterInterface.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018 Couchbase, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.couchbase.client.java; + +import com.couchbase.client.core.Core; +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; +import com.couchbase.client.core.diagnostics.PingResult; +import com.couchbase.client.core.env.Authenticator; +import com.couchbase.client.core.env.PasswordAuthenticator; +import com.couchbase.client.core.env.SeedNode; +import com.couchbase.client.java.analytics.AnalyticsOptions; +//import com.couchbase.client.java.analytics.AnalyticsResult; +import com.couchbase.client.java.diagnostics.DiagnosticsOptions; +import com.couchbase.client.java.diagnostics.PingOptions; +import com.couchbase.client.java.diagnostics.WaitUntilReadyOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.manager.analytics.AnalyticsIndexManager; +import com.couchbase.client.java.manager.bucket.BucketManager; +import com.couchbase.client.java.manager.eventing.EventingFunctionManager; +import com.couchbase.client.java.manager.query.QueryIndexManager; +import com.couchbase.client.java.manager.search.SearchIndexManager; +import com.couchbase.client.java.manager.user.UserManager; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.search.SearchOptions; +import com.couchbase.client.java.search.SearchQuery; +import com.couchbase.client.java.search.result.SearchResult; +import com.couchbase.client.java.transactions.Transactions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; + +import java.time.Duration; +import java.util.Set; +import java.util.function.Supplier; + +import static com.couchbase.client.core.util.Validators.notNull; +import static com.couchbase.client.core.util.Validators.notNullOrEmpty; +import static com.couchbase.client.java.AsyncCluster.extractClusterEnvironment; +import static com.couchbase.client.java.AsyncCluster.seedNodesFromConnectionString; +import static com.couchbase.client.java.ClusterOptions.clusterOptions; + +public interface ClusterInterface { + + AsyncCluster async(); + + ReactiveCluster reactive(); + + @Stability.Volatile + Core core(); + + UserManager users(); + + BucketManager buckets(); + + AnalyticsIndexManager analyticsIndexes(); + + QueryIndexManager queryIndexes(); + + SearchIndexManager searchIndexes(); + + @Stability.Uncommitted + EventingFunctionManager eventingFunctions(); + + ClusterEnvironment environment(); + + QueryResult query(String statement); + + QueryResult query(String statement, QueryOptions options); + + //AnalyticsResult analyticsQuery(String statement); + + // AnalyticsResult analyticsQuery(String statement, AnalyticsOptions options); + + SearchResult searchQuery(String indexName, SearchQuery query); + + SearchResult searchQuery(String indexName, SearchQuery query, SearchOptions options); + + Bucket bucket(String bucketName); + + void disconnect(); + + void disconnect(Duration timeout); + + DiagnosticsResult diagnostics(); + + DiagnosticsResult diagnostics(DiagnosticsOptions options); + + PingResult ping(); + + PingResult ping(PingOptions options); + + void waitUntilReady(Duration timeout); + + void waitUntilReady(Duration timeout, WaitUntilReadyOptions options); + + Transactions transactions(); +} diff --git a/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java new file mode 100644 index 000000000..f0ffd69bd --- /dev/null +++ b/src/main/java/com/couchbase/client/java/transactions/AttemptContextReactiveAccessor.java @@ -0,0 +1,223 @@ +/* +/* + * Copyright 2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.couchbase.client.java.transactions; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionContext; +import com.couchbase.client.core.transaction.CoreTransactionsReactive; +import com.couchbase.client.core.transaction.config.CoreMergedTransactionConfig; +import com.couchbase.client.core.transaction.config.CoreTransactionOptions; +import com.couchbase.client.core.transaction.log.CoreTransactionLogger; +import com.couchbase.client.core.transaction.support.AttemptState; +import com.couchbase.client.java.codec.JsonSerializer; +import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; + +/** + * To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext + * + * @author Michael Reiche + */ +public class AttemptContextReactiveAccessor { + + public static ReactiveTransactions reactive(Transactions transactions) { + try { + Field field = Transactions.class.getDeclaredField("reactive"); + field.setAccessible(true); + return (ReactiveTransactions) field.get(transactions); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static ReactiveTransactionAttemptContext reactive(TransactionAttemptContext atr) { + JsonSerializer serializer; + try { + Field field = TransactionAttemptContext.class.getDeclaredField("serializer"); + field.setAccessible(true); + serializer = (JsonSerializer) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return new ReactiveTransactionAttemptContext(getCore(atr), serializer); + } + + public static TransactionAttemptContext blocking(ReactiveTransactionAttemptContext atr) { + JsonSerializer serializer; + try { + Field field = ReactiveTransactionAttemptContext.class.getDeclaredField("serializer"); + field.setAccessible(true); + serializer = (JsonSerializer) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return new TransactionAttemptContext(getCore(atr), serializer); + } + + public static CoreTransactionLogger getLogger(ReactiveTransactionAttemptContext attemptContextReactive) { + return attemptContextReactive.logger(); + } + + public static CoreTransactionLogger getLogger(TransactionAttemptContext attemptContextReactive) { + return attemptContextReactive.logger(); + } + + // todo gp needed? + @Stability.Internal + public static CoreTransactionAttemptContext newCoreTranactionAttemptContext(ReactiveTransactions transactions) { + + String txnId = UUID.randomUUID().toString(); + CoreTransactionsReactive coreTransactionsReactive; + try { + Field field = ReactiveTransactions.class.getDeclaredField("internal"); + field.setAccessible(true); + coreTransactionsReactive = (CoreTransactionsReactive) field.get(transactions); + } catch (Throwable err) { + throw new RuntimeException(err); + } + + CoreTransactionOptions perConfig = new CoreTransactionOptions(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.of(Duration.ofMinutes(10)), Optional.empty(), Optional.empty()); + + CoreMergedTransactionConfig merged = new CoreMergedTransactionConfig(coreTransactionsReactive.config(), + Optional.ofNullable(perConfig)); + CoreTransactionContext overall = new CoreTransactionContext( + coreTransactionsReactive.core().context().environment().requestTracer(), + coreTransactionsReactive.core().context().environment().eventBus(), UUID.randomUUID().toString(), merged, + coreTransactionsReactive.core().transactionsCleanup()); + + CoreTransactionAttemptContext coreTransactionAttemptContext = coreTransactionsReactive.createAttemptContext(overall, + merged, txnId); + return coreTransactionAttemptContext; + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); + } + + public static ReactiveTransactionAttemptContext from(CoreTransactionAttemptContext coreTransactionAttemptContext, + JsonSerializer serializer) { + TransactionAttemptContext tac = new TransactionAttemptContext(coreTransactionAttemptContext, serializer); + return reactive(tac); + } + + public static CoreTransactionAttemptContext getCore(ReactiveTransactionAttemptContext atr) { + CoreTransactionAttemptContext coreTransactionsReactive; + try { + Field field = ReactiveTransactionAttemptContext.class.getDeclaredField("internal"); + field.setAccessible(true); + coreTransactionsReactive = (CoreTransactionAttemptContext) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return coreTransactionsReactive; + } + + public static CoreTransactionAttemptContext getCore(TransactionAttemptContext atr) { + try { + Field field = TransactionAttemptContext.class.getDeclaredField("internal"); + field.setAccessible(true); + return (CoreTransactionAttemptContext) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static Mono implicitCommit(ReactiveTransactionAttemptContext atr, boolean b) { + CoreTransactionAttemptContext coreTransactionsReactive = getCore(atr); + try { + // getDeclaredMethod() does not find it (because of primitive arg?) + // CoreTransactionAttemptContext.class.getDeclaredMethod("implicitCommit", Boolean.class); + Method[] methods = CoreTransactionAttemptContext.class.getDeclaredMethods(); + Method method = null; + for (Method m : methods) { + if (m.getName().equals("implicitCommit")) { + method = m; + break; + } + } + if (method == null) { + throw new RuntimeException("did not find implicitCommit method"); + } + method.setAccessible(true); + return (Mono) method.invoke(coreTransactionsReactive, b); + } catch (Throwable err) { + throw new RuntimeException(err); + } + + } + + public static AttemptState getState(ReactiveTransactionAttemptContext atr) { + CoreTransactionAttemptContext coreTransactionsReactive = getCore(atr); + try { + Field field = CoreTransactionAttemptContext.class.getDeclaredField("state"); + field.setAccessible(true); + return (AttemptState) field.get(coreTransactionsReactive); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext( + CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) { + return new ReactiveTransactionAttemptContext(core, jsonSerializer); + } + + public static CoreTransactionsReactive getCoreTransactionsReactive(ReactiveTransactions transactions) { + try { + Field field = ReactiveTransactions.class.getDeclaredField("internal"); + field.setAccessible(true); + return (CoreTransactionsReactive) field.get(transactions); + } catch (Throwable err) { + throw new RuntimeException(err); + } + } + + public static TransactionAttemptContext newTransactionAttemptContext(CoreTransactionAttemptContext ctx, + JsonSerializer jsonSerializer) { + return new TransactionAttemptContext(ctx, jsonSerializer); + } + + public static TransactionResult run(Transactions transactions, Consumer transactionLogic, CoreTransactionOptions coreTransactionOptions) { + return reactive(transactions).runBlocking(transactionLogic, coreTransactionOptions); + } + + CoreTransactionAttemptContext coreTransactionsReactive; + try { + Field field = TransactionAttemptContext.class.getDeclaredField("internal"); + field.setAccessible(true); + coreTransactionsReactive = (CoreTransactionAttemptContext) field.get(atr); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return coreTransactionsReactive; + } + + public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext(CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) { + return new ReactiveTransactionAttemptContext(core, jsonSerializer); + } +} diff --git a/src/main/java/com/couchbase/transactions/TransactionsReactive.java b/src/main/java/com/couchbase/transactions/TransactionsReactive.java new file mode 100644 index 000000000..352135ead --- /dev/null +++ b/src/main/java/com/couchbase/transactions/TransactionsReactive.java @@ -0,0 +1,753 @@ +///* +// * Copyright 2021 Couchbase, Inc. +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +//package com.couchbase.transactions; +// +//import com.couchbase.client.core.annotation.Stability; +//import com.couchbase.client.core.cnc.EventBus; +//import com.couchbase.client.core.retry.reactor.DefaultRetry; +//import com.couchbase.client.core.retry.reactor.Jitter; +//import com.couchbase.client.core.retry.reactor.RetryContext; +//import com.couchbase.client.java.Cluster; +//import com.couchbase.client.java.ReactiveCollection; +//import com.couchbase.client.java.ReactiveScope; +//import com.couchbase.client.java.json.JsonObject; +//import com.couchbase.client.java.query.ReactiveQueryResult; +//import com.couchbase.transactions.cleanup.ClusterData; +//import com.couchbase.transactions.cleanup.TransactionsCleanup; +//import com.couchbase.transactions.components.ATR; +//import com.couchbase.transactions.components.ActiveTransactionRecord; +//import com.couchbase.transactions.config.MergedTransactionConfig; +//import com.couchbase.transactions.config.PerTransactionConfig; +//import com.couchbase.transactions.config.PerTransactionConfigBuilder; +//import com.couchbase.transactions.config.SingleQueryTransactionConfig; +//import com.couchbase.transactions.config.SingleQueryTransactionConfigBuilder; +//import com.couchbase.transactions.config.TransactionConfig; +//import com.couchbase.transactions.deferred.TransactionSerializedContext; +//import com.couchbase.transactions.error.TransactionCommitAmbiguous; +//import com.couchbase.transactions.error.TransactionExpired; +//import com.couchbase.transactions.error.TransactionFailedException; +//import com.couchbase.transactions.error.internal.ErrorClasses; +//import com.couchbase.transactions.error.external.TransactionOperationFailed; +//import com.couchbase.transactions.forwards.Supported; +//import com.couchbase.transactions.log.EventBusPersistedLogger; +//import com.couchbase.transactions.log.PersistedLogWriter; +//import com.couchbase.transactions.log.TransactionLogEvent; +//import com.couchbase.transactions.support.AttemptContextFactory; +//import com.couchbase.transactions.support.AttemptState; +//import com.couchbase.transactions.support.OptionsWrapperUtil; +//import com.couchbase.transactions.util.DebugUtil; +//import reactor.core.publisher.Mono; +//import reactor.core.scheduler.Schedulers; +// +//import java.time.Duration; +//import java.time.temporal.ChronoUnit; +//import java.util.Objects; +//import java.util.Optional; +//import java.util.UUID; +//import java.util.concurrent.TimeUnit; +//import java.util.concurrent.atomic.AtomicReference; +//import java.util.function.Consumer; +//import java.util.function.Function; +//import java.util.function.Predicate; +// +//import static com.couchbase.transactions.error.internal.TransactionOperationFailedBuilder.createError; +//import static com.couchbase.transactions.log.PersistedLogWriter.MAX_LOG_ENTRIES_DEFAULT; +//import static com.couchbase.transactions.support.SpanWrapperUtil.DB_COUCHBASE_TRANSACTIONS; +// +///** +// * An asynchronous version of {@link Transactions}, allowing transactions to be created and run in an asynchronous +// * manner. +// *

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

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

    +// *
  • The transaction logic is supplied with a {@link ReactiveTransactionAttemptContext}, which contains asynchronous +// * methods to allow it to read, mutate, insert and delete documents, as well as commit or rollback the +// * transactions.
  • +// *
  • The transaction logic should run these methods as a Reactor chain.
  • +// *
  • The transaction logic should return a Mono{@literal <}Void{@literal >}. Any +// * Flux or Mono can be converted to a Mono{@literal <}Void{@literal >} by +// * calling .then() on it.
  • +// *
  • This method returns a Mono{@literal <}TransactionResult{@literal >}, which should be handled +// * as a normal Reactor Mono.
  • +// *
+// * +// * @param transactionLogic the application's transaction logic +// * @param perConfig the configuration to use for this transaction +// * @return there is no need to check the returned {@link TransactionResult}, as success is implied by the lack of a +// * thrown exception. It contains information useful only for debugging and logging. +// * @throws TransactionFailedException or a derived exception if the transaction fails to commit for any reason, possibly +// * after multiple retries. The exception contains further details of the error. Not +// */ +// public Mono run(Function> transactionLogic, +// PerTransactionConfig perConfig) { +// return Mono.defer(() -> { +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// +// TransactionContext overall = +// new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), +// cleanup.clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), +// now(), +// Duration.ZERO, +// merged); +// AtomicReference startTime = new AtomicReference<>(0L); +// +// Mono ob = Mono.fromCallable(() -> { +// String txnId = UUID.randomUUID().toString(); +// overall.LOGGER.info(configDebug(config, perConfig)); +// return createAttemptContext(overall, merged, txnId); +// }).flatMap(ctx -> { +// ctx.LOGGER.info("starting attempt %d/%s/%s", +// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); +// Mono result = transactionLogic.apply(ctx); +// return result +// .onErrorResume(err -> { +// ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in async, rethrowing", err); +// logElidedStacktrace(ctx, err); +// +// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); +// }) +// .thenReturn(ctx); +// }).doOnSubscribe(v -> startTime.set(System.nanoTime())) +// .doOnNext(v -> v.LOGGER.trace(v.attemptId(), "finished attempt %d in %sms", +// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000)); +// +// return executeTransaction(merged, overall, ob) +// .doOnNext(v -> overall.span().finish()) +// .doOnError(err -> overall.span().failWith(err)); +// }); +// } +// +// // Printing the stacktrace is expensive in terms of log noise, but has been a life saver on many debugging +// // encounters. Strike a balance by eliding the more useless elements. +// // TODO: changed from private to public +// public void logElidedStacktrace(ReactiveTransactionAttemptContext ctx, Throwable err) { +// DebugUtil.fetchElidedStacktrace(err, (s) -> ctx.LOGGER.info(ctx.attemptId(), " " + s.toString())); +// } +// +// // TODO: changed from private to public +// public static String configDebug(TransactionConfig config, PerTransactionConfig perConfig) { +// StringBuilder sb = new StringBuilder(); +// sb.append("library version: "); +// sb.append(TransactionsReactive.class.getPackage().getImplementationVersion()); +// sb.append(" config: "); +// sb.append("atrs="); +// sb.append(config.numAtrs()); +// sb.append(", metadataCollection="); +// sb.append(config.metadataCollection()); +// sb.append(", expiry="); +// sb.append(perConfig.expirationTime().orElse(config.transactionExpirationTime()).toMillis()); +// sb.append("msecs durability="); +// sb.append(config.durabilityLevel()); +// sb.append(" per-txn config="); +// sb.append(" durability="); +// sb.append(perConfig.durabilityLevel()); +// sb.append(", supported="); +// sb.append(Supported.SUPPORTED); +// return sb.toString(); +// } +// +// /** +// * Convenience overload that runs {@link TransactionsReactive#run} with a default PerTransactionConfig. +// */ +// public Mono run(Function> transactionLogic) { +// return run(transactionLogic, PerTransactionConfigBuilder.create().build()); +// } +// +// @Stability.Volatile +// public Mono commit(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { +// return deferred(serialized, +// perConfig, +// // Nothing to actually do, just want the implicit commit +// (ctx) -> Mono.empty()); +// } +// +// @Stability.Volatile +// public Mono rollback(TransactionSerializedContext serialized, PerTransactionConfig perConfig) { +// return deferred(serialized, +// perConfig, +// (ctx) -> ctx.rollback()); +// } +// +// @Stability.Volatile +// private Mono deferred(TransactionSerializedContext serialized, +// PerTransactionConfig perConfig, +// Function> initial) { +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// JsonObject hydrated = JsonObject.fromJson(serialized.encodeAsString()); +// +// String atrBucket = hydrated.getString("atrBucket"); +// String atrScope = hydrated.getString("atrScope"); +// String atrCollectionName = hydrated.getString("atrCollection"); +// String atrId = hydrated.getString("atrId"); +// ReactiveCollection atrCollection = cleanup.clusterData() +// .getBucketFromName(atrBucket) +// .scope(atrScope) +// .collection(atrCollectionName); +// +// return ActiveTransactionRecord.getAtr(atrCollection, +// atrId, +// OptionsWrapperUtil.kvTimeoutNonMutating(merged, atrCollection.core()), +// null) +// +// .flatMap(atrOpt -> { +// if (!atrOpt.isPresent()) { +// return Mono.error(new IllegalStateException(String.format("ATR %s/%s could not be found", +// atrBucket, atrId))); +// } +// else { +// ATR atr = atrOpt.get(); +// +// // Note startTimeServerMillis is written with ${Mutation.CAS} while currentTimeServer +// // could have come from $vbucket.HLC and is hence one-second granularity. So, this is a +// // somewhat imperfect comparison. +// Duration currentTimeServer = Duration.ofNanos(atr.cas()); +// Duration startTimeServer = Duration.ofMillis(hydrated.getLong("startTimeServerMillis")); +// +// // This includes the time elapsed during the first part of the transaction, plus any time +// // elapsed during the period the transaction was expired. Total time since the transaction +// // began, basically. +// Duration timeElapsed = currentTimeServer.minus(startTimeServer); +// +// TransactionContext overall = +// new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), +// cleanup.clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), +// Duration.ofNanos(System.nanoTime()), +// timeElapsed, +// merged); +// AtomicReference startTime = new AtomicReference<>(0L); +// +// overall.LOGGER.info("elapsed time = %dmsecs (ATR start time %dmsecs, current ATR time %dmsecs)", +// timeElapsed.toMillis(), startTimeServer.toMillis(), currentTimeServer.toMillis()); +// +// Mono ob = Mono.defer(() -> { +// ReactiveTransactionAttemptContext ctx = attemptContextFactory.createFrom(hydrated, overall, merged, this); +// ctx.LOGGER.info("starting attempt %d/%s/%s", +// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); +// ctx.LOGGER.info(configDebug(config, perConfig)); +// +// return initial.apply(ctx) +// +// // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it +// .subscribeOn(Schedulers.elastic()) +// +// .onErrorResume(err -> { +// ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in deferred, rethrowing", +// err); +// +// logElidedStacktrace(ctx, err); +// +// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); +// }) +// +// .doOnSubscribe(v -> startTime.set(System.nanoTime())) +// +// .doOnNext(v -> { +// ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", +// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); +// }) +// +// .thenReturn(ctx); +// }); +// +// return executeTransaction(merged, overall, ob) +// .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) +// .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); +// } +// }); +// } +// +// Mono runBlocking(Consumer txnLogic, PerTransactionConfig perConfig) { +// return Mono.defer(() -> { +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// TransactionContext overall = +// new TransactionContext(cleanup.clusterData().cluster().environment().requestTracer(), +// cleanup.clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), +// now(), +// Duration.ZERO, +// merged); +// AtomicReference startTime = new AtomicReference<>(0L); +// overall.LOGGER.info(configDebug(config, perConfig)); +// +// Mono ob = Mono.defer(() -> { +// String txnId = UUID.randomUUID().toString(); +// ReactiveTransactionAttemptContext ctx = createAttemptContext(overall, merged, txnId); +// TransactionAttemptContext ctxBlocking = new TransactionAttemptContext(ctx); +// ctx.LOGGER.info("starting attempt %d/%s/%s", +// overall.numAttempts(), ctx.transactionId(), ctx.attemptId()); +// +// return Mono.fromRunnable(() -> txnLogic.accept(ctxBlocking)) +// +// // TXNJ-50: Make sure we run user's blocking logic on a scheduler that can take it +// .subscribeOn(Schedulers.elastic()) +// +// .onErrorResume(err -> { +// ctx.LOGGER.info(ctx.attemptId(), "caught exception '%s' in runBlocking, rethrowing", err); +// +// logElidedStacktrace(ctx, err); +// +// return Mono.error(TransactionOperationFailed.convertToOperationFailedIfNeeded(err, ctx)); +// }) +// +// .doOnSubscribe(v -> startTime.set(System.nanoTime())) +// +// .doOnNext(v -> { +// ctx.LOGGER.trace(ctx.attemptId(), "finished attempt %d in %sms", +// overall.numAttempts(), (System.nanoTime() - startTime.get()) / 1_000_000); +// }) +// +// .thenReturn(ctx); +// }); +// +// return executeTransaction(merged, overall, ob) +// .doOnNext(v -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).finish()) +// .doOnError(err -> overall.span().attribute(DB_COUCHBASE_TRANSACTIONS + "retries", overall.numAttempts()).failWith(err)); +// }); +// } +// +// public TransactionConfig config() { +// return config; +// } +// +// private static Duration now() { +// return Duration.of(System.nanoTime(), ChronoUnit.NANOS); +// } +// +// TransactionsCleanup cleanup() { +// return cleanup; +// } +// +// private void addCleanupRequestForContext(ReactiveTransactionAttemptContext ctx) { +// // Whether the txn was successful or not, still want to clean it up +// if (ctx.queryMode()) { +// ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as in query mode"); +// } +// else if (ctx.serialized().isPresent()) { +// ctx.LOGGER.info(ctx.attemptId(), "Skipping cleanup request as deferred transaction"); +// } +// else if (ctx.atrId().isPresent() && ctx.atrCollection().isPresent()) { +// switch (ctx.state()) { +// case NOT_STARTED: +// case COMPLETED: +// case ROLLED_BACK: +// ctx.LOGGER.trace(ctx.attemptId(), "Skipping addition of cleanup request in state %s", ctx.state()); +// break; +// default: +// ctx.LOGGER.trace(ctx.attemptId(), "Adding cleanup request for %s/%s", +// ctx.atrCollection().get().name(), ctx.atrId().get()); +// +// cleanup.add(ctx.createCleanupRequest()); +// } +// } else { +// // No ATR entry to remove +// ctx.LOGGER.trace(ctx.attemptId(), "Skipping cleanup request as no ATR entry to remove (due to no " + +// "mutations)"); +// } +// } +// +// private static TransactionResult createResultFromContext(TransactionContext overall) { +// return new TransactionResult(overall.attempts(), +// overall.LOGGER, +// Duration.of(System.nanoTime() - overall.startTimeClient().toNanos(), ChronoUnit.NANOS), +// overall.transactionId(), +// overall.serialized()); +// } +// +// /** +// * Performs a single query transaction, with default configuration. +// * +// * @param statement the statement to execute. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(String statement) { +// return query(null, statement, SingleQueryTransactionConfigBuilder.create().build()); +// } +// +// /** +// * Performs a single query transaction, with a custom configuration. +// * +// * @param statement the statement to execute. +// * @param queryOptions configuration options. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(String statement, SingleQueryTransactionConfig queryOptions) { +// return query(null, statement, queryOptions); +// } +// +// /** +// * Performs a single query transaction, with a scope context and default configuration. +// * +// * @param statement the statement to execute. +// * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope +// * rather than needed to provide the full keyspace. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(ReactiveScope scope, String statement) { +// return query(scope, statement, SingleQueryTransactionConfigBuilder.create().build()); +// } +// +// /** +// * Performs a single query transaction, with a scope context and custom configuration. +// * +// * @param statement the statement to execute. +// * @param scope the query will be executed in the context of this scope, so it can refer to a collection on this scope +// * rather than needed to provide the full keyspace. +// * @param queryOptions configuration options. +// * @return a ReactiveSingleQueryTransactionResult +// */ +// @Stability.Uncommitted +// public Mono query(ReactiveScope scope, String statement, SingleQueryTransactionConfig queryOptions) { +// return Mono.defer(() -> { +// AtomicReference queryResult = new AtomicReference<>(); +// return run((ctx) -> ctx.query(scope, statement, queryOptions.queryOptions(), true) +// .doOnNext(qr -> queryResult.set(qr)) +// .then(), queryOptions.convert()) +// .map(result -> new ReactiveSingleQueryTransactionResult(result.log(), queryResult.get())); +// }); +// } +// +// @Stability.Internal +// @Deprecated // Prefer setting TransactionConfigBuilder#testFactories now +// public void setAttemptContextFactory(AttemptContextFactory factory) { +// this.attemptContextFactory = factory; +// } +// public ReactiveTransactionAttemptContext newAttemptContextReactive(){ +// PerTransactionConfig perConfig = PerTransactionConfigBuilder.create().build(); +// MergedTransactionConfig merged = new MergedTransactionConfig(config, Optional.of(perConfig)); +// +// TransactionContext overall = new TransactionContext( +// cleanup().clusterData().cluster().environment().requestTracer(), +// cleanup().clusterData().cluster().environment().eventBus(), +// UUID.randomUUID().toString(), now(), Duration.ZERO, merged); +// +// String txnId = UUID.randomUUID().toString(); +// overall.LOGGER.info(configDebug(config, perConfig)); +// return createAttemptContext(overall, merged, txnId); +// } +// +//} diff --git a/src/main/java/com/example/demo/CouchbaseTransactionManager.pre-core b/src/main/java/com/example/demo/CouchbaseTransactionManager.pre-core new file mode 100644 index 000000000..ab9d84087 --- /dev/null +++ b/src/main/java/com/example/demo/CouchbaseTransactionManager.pre-core @@ -0,0 +1,201 @@ +package com.example.demo; + +import java.util.concurrent.atomic.AtomicReference; + +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.transaction.ClientSession; +import org.springframework.data.couchbase.transaction.ClientSessionImpl; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; + +// todo gp why is there separate CouchbaseCallbackTransactionManager if this class also extends CallbackPreferringPlatformTransactionManager? +// todo gp there is another CouchbaseTransactionManager in another package, which is valid? +public class CouchbaseTransactionManager extends AbstractPlatformTransactionManager + implements DisposableBean, ResourceTransactionManager, CallbackPreferringPlatformTransactionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); + + private final CouchbaseTemplate template; + + public CouchbaseTransactionManager(CouchbaseTemplate template) { + this.template = template; + } + + public CouchbaseTransactionalTemplate template() { + return new CouchbaseTransactionalTemplate(template); + } + + @Override + public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { + final AtomicReference result = new AtomicReference<>(); + // todo gp like CouchbaseCallbackTransactionManager, it needs access to CouchbaseClientFactory here (Cluster) +// TransactionResult txnResult = transactions.run(attemptContext -> { +// +// if (TransactionSynchronizationManager.hasResource(template.getCouchbaseClientFactory())) { +// ((CouchbaseResourceHolder) TransactionSynchronizationManager +// .getResource(template.reactive().getCouchbaseClientFactory())) +// .setAttemptContext(attemptContext); +// } else { +// TransactionSynchronizationManager.bindResource( +// template.reactive().getCouchbaseClientFactory(), +// new CouchbaseResourceHolder(attemptContext) +// ); +// } +// +// try { +// // Since we are on a different thread now transparently, at least make sure +// // that the original method invocation is synchronized. +// synchronized (this) { +// result.set(callback.doInTransaction(null)); +// } +// } catch (RuntimeException e) { +// System.err.println("RuntimeException: "+e+" instanceof RuntimeException "+(e instanceof RuntimeException)); +// throw e; +// } catch (Throwable e) { +// System.err.println("RuntimeException: "+e+" instanceof "+(e instanceof Throwable)); +// throw new RuntimeException(e); +// } +// }); + +// LOGGER.debug("Completed Couchbase Transaction with Result: " + txnResult); + return result.get(); + } + + @Override + protected CouchbaseTransactionObject doGetTransaction() throws TransactionException { + CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + return new CouchbaseTransactionObject(resourceHolder); + } + + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractTransaction(transaction).hasResourceHolder(); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + LOGGER.debug("Beginning Couchbase Transaction with Definition {}", definition); + } + + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + LOGGER.debug("Committing Couchbase Transaction with status {}", status); + } + + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + LOGGER.warn("Rolling back Couchbase Transaction with status {}", status); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + LOGGER.trace("Performing cleanup of Couchbase Transaction {}", transaction); + } + + @Override + public void destroy() { + } + + @Override + public Object getResourceFactory() { + return template.getCouchbaseClientFactory(); + } + + private static CouchbaseTransactionObject extractTransaction(Object transaction) { + Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + transaction.getClass())); + + return (CouchbaseTransactionObject) transaction; + } + + public static class CouchbaseResourceHolder extends ResourceHolderSupport { + + private volatile TransactionAttemptContext attemptContext; + private volatile ReactiveTransactionAttemptContext attemptContextReactive; + private volatile ClientSession session = new ClientSessionImpl(); + + public CouchbaseResourceHolder(TransactionAttemptContext attemptContext) { + this.attemptContext = attemptContext; + } + + public TransactionAttemptContext getAttemptContext() { + return attemptContext; + } + + public void setAttemptContext(TransactionAttemptContext attemptContext) { + this.attemptContext = attemptContext; + } + + public ReactiveTransactionAttemptContext getAttemptContextReactive() { + return attemptContext!= null ? AttemptContextReactiveAccessor.getACR(attemptContext) : attemptContextReactive; + } + public void setAttemptContextReactive(ReactiveTransactionAttemptContext attemptContextReactive) { + this.attemptContextReactive = attemptContextReactive; + } + + public ClientSession getSession() { + return session; + } + + public void setSession(ClientSession session){ + this.session = session; + } + + @Override + public String toString() { + return "CouchbaseResourceHolder{" + + "attemptContext=" + attemptContext + + '}'; + } + + } + + protected static class CouchbaseTransactionObject implements SmartTransactionObject { + + final CouchbaseResourceHolder resourceHolder; + + CouchbaseTransactionObject(CouchbaseResourceHolder resourceHolderIn) { + resourceHolder = resourceHolderIn; + } + + @Override + public boolean isRollbackOnly() { + return resourceHolder != null && resourceHolder.isRollbackOnly(); + } + + @Override + public void flush() { + TransactionSynchronizationUtils.triggerFlush(); + } + + public boolean hasResourceHolder() { + return resourceHolder != null; + } + + @Override + public String toString() { + return "CouchbaseTransactionObject{" + + "resourceHolder=" + resourceHolder + + '}'; + } + } + +} diff --git a/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java new file mode 100644 index 000000000..c0098a9d8 --- /dev/null +++ b/src/main/java/com/example/demo/CouchbaseTransactionalTemplate.java @@ -0,0 +1,67 @@ +package com.example.demo; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.codec.Transcoder; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + +public class CouchbaseTransactionalTemplate { + + private final CouchbaseTemplate template; + + public CouchbaseTransactionalTemplate(CouchbaseTemplate template) { + this.template = template; + } + + public SpringTransactionGetResult findById(String id, Class domainType) { + try { + CoreTransactionAttemptContext ctx = getContext(); + CoreTransactionGetResult getResult = ctx.get( makeCollectionIdentifier(template.getCouchbaseClientFactory().getDefaultCollection().async()) , id).block(); + + T t = template.support().decodeEntity(id, new String(getResult.contentAsBytes()), getResult.cas(), domainType, + null, null, null); + return new SpringTransactionGetResult<>(t, getResult); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + + } + + public void replaceById(CoreTransactionGetResult getResult, T entity) { + CoreTransactionAttemptContext ctx = getContext(); + Transcoder transCoder = template.getCouchbaseClientFactory().getCluster().environment().transcoder(); + Transcoder.EncodedValue encoded = transCoder.encode(template.support().encodeEntity(entity).export()); + ctx.replace(getResult, encoded.encoded()); + } + + private CoreTransactionAttemptContext getContext() { + ReactiveCouchbaseResourceHolder resource = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + CoreTransactionAttemptContext atr; + if (resource != null) { + atr = resource.getCore(); + } else { + ReactiveCouchbaseResourceHolder holder = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory().getCluster()); + atr = holder.getCore(); + } + return atr; + } + + + public static ReactiveCouchbaseResourceHolder getSession(ReactiveCouchbaseTemplate template) { + ReactiveCouchbaseResourceHolder resource = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(template.getCouchbaseClientFactory()); + return resource; + } + +} diff --git a/src/main/java/com/example/demo/SpringTransactionGetResult.java b/src/main/java/com/example/demo/SpringTransactionGetResult.java new file mode 100644 index 000000000..27ede4aaf --- /dev/null +++ b/src/main/java/com/example/demo/SpringTransactionGetResult.java @@ -0,0 +1,31 @@ +package com.example.demo; + +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.transactions.TransactionGetResult; + +public class SpringTransactionGetResult { + + private final T value; + private final CoreTransactionGetResult inner; + + public SpringTransactionGetResult(T value, CoreTransactionGetResult inner) { + this.value = value; + this.inner = inner; + } + + public T getValue() { + return value; + } + + public CoreTransactionGetResult getInner() { + return inner; + } + + @Override + public String toString() { + return "SpringTransactionGetResult{" + + "value=" + value + + ", inner=" + inner + + '}'; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java index 51781a7d8..70a6c9227 100644 --- a/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/CouchbaseClientFactory.java @@ -18,6 +18,9 @@ import java.io.Closeable; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import com.couchbase.client.java.Bucket; @@ -73,4 +76,10 @@ public interface CouchbaseClientFactory extends Closeable { */ PersistenceExceptionTranslator getExceptionTranslator(); + CoreTransactionAttemptContext getCore(TransactionOptions options, + CoreTransactionAttemptContext atr); + + //CouchbaseClientFactory with(CouchbaseTransactionalOperator txOp); + + //CouchbaseTransactionalOperator getTransactionalOperator(); } diff --git a/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java new file mode 100644 index 000000000..13f0f9773 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/ReactiveCouchbaseClientFactory.java @@ -0,0 +1,130 @@ +/* + * Copyright 2016-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterInterface; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.Scope; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; +import reactor.core.publisher.Mono; + +import org.springframework.dao.support.PersistenceExceptionTranslator; + +import java.io.IOException; + +/** + * Interface for factories creating reactive {@link Cluster} instances. + * + * @author Mark Paluch + * @author Christoph Strobl + * @author Mathieu Ouellet + * @since 2.0 + */ +public interface ReactiveCouchbaseClientFactory /*extends CodecRegistryProvider*/ { + + /** + * Provides access to the managed SDK {@link Cluster} reference. + */ + Mono getCluster(); + + /** + * Provides access to the managed SDK {@link Cluster} reference. + */ + ClusterInterface getBlockingCluster(); + + /** + * Provides access to the managed SDK {@link Bucket} reference. + */ + Mono getBucket(); + + /** + * Provides access to the managed SDK {@link Scope} reference. + */ + Mono getScope(); + + /** + * Provides access to the managed SDK {@link Scope} reference without block() + */ + Scope getBlockingScope(String scopeName); + + /** + * Provides access to a collection (identified by its name) in managed SDK {@link Scope} reference. + * + * @param name the name of the collection. If null is passed in, the default collection is assumed. + */ + Mono getCollection(String name); + + /** + * Provides access to a collection (identified by its name) without block() + * + * @param name the name of the collection. If null is passed in, the default collection is assumed. + */ + Collection getBlockingCollection(String collectionName); + + /** + * Provides access to the default collection. + */ + Mono getDefaultCollection(); + + /** + * Returns a new {@link CouchbaseClientFactory} set to the scope given as an argument. + * + * @param scopeName the name of the scope to use for all collection access. + * @return a new client factory, bound to the other scope. + */ + ReactiveCouchbaseClientFactory withScope(String scopeName); + + /** + * The exception translator used on the factory. + */ + PersistenceExceptionTranslator getExceptionTranslator(); + + Mono getTransactionResources(TransactionOptions options); + + String getBucketName(); + + String getScopeName(); + + void close() throws IOException; + + ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, CoreTransactionAttemptContext ctx); + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.ClientSession) + */ + ReactiveCouchbaseClientFactory withCore(ReactiveCouchbaseResourceHolder core); + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#with(com.mongodb.session.ClientSession) + */ + ReactiveCouchbaseClientFactory with(CouchbaseTransactionalOperator txOp); + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#isTransactionActive() + */ + boolean isTransactionActive(); + + CouchbaseTransactionalOperator getTransactionalOperator(); +} diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index b97b57f95..f6d702447 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -15,8 +15,12 @@ */ package org.springframework.data.couchbase; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.function.Supplier; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.config.TransactionOptions; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; @@ -28,7 +32,11 @@ import com.couchbase.client.java.ClusterOptions; import com.couchbase.client.java.Collection; import com.couchbase.client.java.Scope; +import com.couchbase.client.java.codec.JsonSerializer; import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; /** * The default implementation of a {@link CouchbaseClientFactory}. @@ -42,32 +50,38 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { private final Bucket bucket; private final Scope scope; private final PersistenceExceptionTranslator exceptionTranslator; + //private JsonSerializer serializer = null; public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName) { + final String bucketName) { this(connectionString, authenticator, bucketName, null); } public SimpleCouchbaseClientFactory(final String connectionString, final Authenticator authenticator, - final String bucketName, final String scopeName) { - this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator))), + final String bucketName, final String scopeName) { + this(new OwnedSupplier<>(Cluster.connect(connectionString, ClusterOptions.clusterOptions(authenticator) + // todo gp disabling cleanupLostAttempts to simplify output during development + .environment(env -> env.transactionsConfig( + TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false)))))), bucketName, scopeName); } 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))), bucketName, scopeName); + //this.serializer = environment.jsonSerializer(); } public SimpleCouchbaseClientFactory(final Cluster cluster, final String bucketName, final String scopeName) { this(() -> cluster, bucketName, scopeName); + //this.serializer = cluster.environment().jsonSerializer(); } private SimpleCouchbaseClientFactory(final Supplier cluster, final String bucketName, - final String scopeName) { + final String scopeName) { this.cluster = cluster; this.bucket = cluster.get().bucket(bucketName); this.scope = scopeName == null ? bucket.defaultScope() : bucket.scope(scopeName); @@ -97,9 +111,9 @@ public Scope getScope() { @Override public Collection getCollection(final String collectionName) { final Scope scope = getScope(); - if (collectionName == null) { + if (collectionName == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionName)) { if (!scope.name().equals(CollectionIdentifier.DEFAULT_SCOPE)) { - throw new IllegalStateException("A collectionName must be provided if a non-default scope is used!"); + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used"); } return getBucket().defaultCollection(); } @@ -117,10 +131,37 @@ public PersistenceExceptionTranslator getExceptionTranslator() { } @Override - public void close() { - if (cluster instanceof OwnedSupplier) { - cluster.get().disconnect(); + public CoreTransactionAttemptContext getCore(TransactionOptions options, CoreTransactionAttemptContext atr) { + // can't we just use AttemptContextReactive everywhere? Instead of creating AttemptContext(atr), then + // accessing at.getACR() ? + if (atr == null) { + atr = AttemptContextReactiveAccessor + .newCoreTranactionAttemptContext(AttemptContextReactiveAccessor.reactive(getCluster().transactions())); } + + return atr; + } + + // @Override + // public CouchbaseClientFactory with(CouchbaseStuffHandle txOp) { + // return new SimpleCouchbaseClientFactory(cluster, bucket.name(), scope.name(), txOp); + // } + + // @Override + // public CouchbaseStuffHandle getTransactionalOperator() { + // return (CouchbaseStuffHandle) transactionalOperator; + // } + + @Override + public void close() { + // todo gp + // if (cluster instanceof OwnedSupplier) { + // cluster.get().disconnect(); + // } + } + + private static Duration now() { + return Duration.of(System.nanoTime(), ChronoUnit.NANOS); } } diff --git a/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java new file mode 100644 index 000000000..5e90fc7e7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/SimpleReactiveCouchbaseClientFactory.java @@ -0,0 +1,402 @@ +package org.springframework.data.couchbase; + +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.ClusterInterface; +import com.couchbase.client.java.codec.JsonSerializer; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.Transactions; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; +import reactor.core.publisher.Mono; + +import java.io.IOException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.couchbase.core.CouchbaseExceptionTranslator; +import org.springframework.data.couchbase.transaction.SessionAwareMethodInterceptor; +import org.springframework.util.ObjectUtils; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.Scope; + +public class SimpleReactiveCouchbaseClientFactory implements ReactiveCouchbaseClientFactory { + final Mono cluster; + final ClusterInterface theCluster; + final String bucketName; + final String scopeName; + final PersistenceExceptionTranslator exceptionTranslator; + JsonSerializer serializer; + Transactions transactions; + CouchbaseTransactionalOperator transactionalOperator; + + public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName, + CouchbaseTransactionalOperator transactionalOperator) { + this.cluster = Mono.just(cluster); + this.theCluster = cluster; + this.bucketName = bucketName; + this.scopeName = scopeName; + this.exceptionTranslator = new CouchbaseExceptionTranslator(); + this.serializer = cluster.environment().jsonSerializer(); + this.transactions = cluster.transactions(); + this.transactionalOperator = transactionalOperator; + } + + public SimpleReactiveCouchbaseClientFactory(Cluster cluster, String bucketName, String scopeName) { + this(cluster, bucketName, scopeName, null); + } + + @Override + public Mono getCluster() { + return cluster; + } + + + @Override + public ClusterInterface getBlockingCluster() { + return theCluster; + } + + @Override + public Mono getBucket() { + return cluster.map((c) -> c.bucket(bucketName)); + } + + @Override + public String getBucketName() { + return bucketName; + } + + @Override + public Mono getScope() { + return cluster.map((c) -> c.bucket(bucketName).scope(scopeName != null ? scopeName : DEFAULT_SCOPE)); + } + + @Override + public Scope getBlockingScope(String scopeName) { + return theCluster.bucket(bucketName).scope(scopeName != null ? scopeName : (this.scopeName != null ? this.scopeName : DEFAULT_SCOPE)); + } + + @Override + public String getScopeName() { + return scopeName; + } + + @Override + public Mono getCollection(String collectionName) { + if (getScopeName() != null && !DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName == null || DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used."); + } + } + if (getScopeName() == null || DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName != null && !DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException( + "A collectionName must be null or " + DEFAULT_COLLECTION + " if scope is null or " + DEFAULT_SCOPE); + } + } + return getScope().map((s) -> s.collection(collectionName != null ? collectionName : DEFAULT_COLLECTION)); + } + + @Override + public Collection getBlockingCollection(String collectionName) { + if (getScopeName() != null && !DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName == null || DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used."); + } + } + if (getScopeName() == null || DEFAULT_SCOPE.equals(getScopeName())) { + if (collectionName != null && !DEFAULT_COLLECTION.equals(collectionName)) { + throw new IllegalStateException( + "A collectionName must be null or " + DEFAULT_COLLECTION + " if scope is null or " + DEFAULT_SCOPE); + } + } + return theCluster.bucket(bucketName).scope(scopeName != null ? scopeName : DEFAULT_SCOPE).collection(collectionName != null ? collectionName : DEFAULT_COLLECTION); + } + + @Override + public Mono getDefaultCollection() { + if (getScopeName() != null && DEFAULT_SCOPE.equals(getScopeName())) { + throw new IllegalStateException("A collectionName must be provided if a non-default scope is used."); + } + return cluster.map((c) -> c.bucket(bucketName).defaultCollection()); + } + + @Override + public ReactiveCouchbaseClientFactory withScope(String scopeName) { + return new SimpleReactiveCouchbaseClientFactory((Cluster) cluster.block(), bucketName, + scopeName != null ? scopeName : this.scopeName); + } + + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return exceptionTranslator; + } + + @Override + public void close() { + cluster.block().disconnect(); + } + + @Override + public Mono getTransactionResources(TransactionOptions options) { + return Mono.just(new ReactiveCouchbaseResourceHolder(null)); + } + + @Override + public ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, + CoreTransactionAttemptContext atr) { + if (atr == null) { + atr = AttemptContextReactiveAccessor + .newCoreTranactionAttemptContext(AttemptContextReactiveAccessor.reactive(transactions)); + } + return new ReactiveCouchbaseResourceHolder(atr); + } + + @Override + public ReactiveCouchbaseClientFactory withCore(ReactiveCouchbaseResourceHolder holder) { + return new CoreTransactionAttemptContextBoundCouchbaseClientFactory(holder, this, transactions); + } + + @Override + public boolean isTransactionActive() { + return false; + } + + @Override + public CouchbaseTransactionalOperator getTransactionalOperator() { + return transactionalOperator; + } + + @Override + public ReactiveCouchbaseClientFactory with(CouchbaseTransactionalOperator txOp) { + return new SimpleReactiveCouchbaseClientFactory((Cluster) getCluster().block(), bucketName, scopeName, txOp); + } + + private T createProxyInstance(ReactiveCouchbaseResourceHolder session, T target, Class targetType) { + + ProxyFactory factory = new ProxyFactory(); + factory.setTarget(target); + factory.setInterfaces(targetType); + factory.setOpaque(true); + + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ReactiveCouchbaseResourceHolder.class, + ClusterInterface.class, this::proxyDatabase, Collection.class, this::proxyCollection)); + + return targetType.cast(factory.getProxy(target.getClass().getClassLoader())); + } + + private Collection proxyCollection(ReactiveCouchbaseResourceHolder session, Collection c) { + return createProxyInstance(session, c, Collection.class); + } + + private ClusterInterface proxyDatabase(ReactiveCouchbaseResourceHolder session, ClusterInterface cluster) { + return createProxyInstance(session, cluster, ClusterInterface.class); + } + + /** + * {@link CoreTransactionAttemptContext} bound TODO decorating the database with a + * {@link SessionAwareMethodInterceptor}. + * + * @author Christoph Strobl + * @since 2.1 + */ + static final class CoreTransactionAttemptContextBoundCouchbaseClientFactory + implements ReactiveCouchbaseClientFactory { + + private final ReactiveCouchbaseResourceHolder transactionResources; + private final ReactiveCouchbaseClientFactory delegate; + // private final Transactions transactions; + + CoreTransactionAttemptContextBoundCouchbaseClientFactory(ReactiveCouchbaseResourceHolder transactionResources, + ReactiveCouchbaseClientFactory delegate, Transactions transactions) { + this.transactionResources = transactionResources; + this.delegate = delegate; + // this.transactions = transactions; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getMongoDatabase() + */ + @Override + public Mono getCluster() throws DataAccessException { + return delegate.getCluster().map(this::decorateDatabase); + } + + @Override + public ClusterInterface getBlockingCluster() throws DataAccessException { + return decorateDatabase(delegate.getBlockingCluster()); + } + + @Override + public Mono getBucket() { + return delegate.getBucket(); + } + + @Override + public Mono getScope() { + return delegate.getScope(); + } + + @Override + public Mono getCollection(String name) { + return delegate.getCollection(name); + } + + @Override + public Collection getBlockingCollection(String collectionName) { + return delegate.getBlockingCollection(collectionName); + } + + @Override + public Scope getBlockingScope(String scopeName) { + return delegate.getBlockingScope(scopeName); + } + @Override + public Mono getDefaultCollection() { + return delegate.getDefaultCollection(); + } + + @Override + public ReactiveCouchbaseClientFactory withScope(String scopeName) { + return delegate.withScope(scopeName); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getExceptionTranslator() + */ + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return delegate.getExceptionTranslator(); + } + + @Override + public String getBucketName() { + return delegate.getBucketName(); + } + + @Override + public String getScopeName() { + return delegate.getScopeName(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getSession(com.mongodb.CoreTransactionAttemptContextOptions) + */ + + @Override + public Mono getTransactionResources(TransactionOptions options) { + return Mono.just(transactionResources); + } + + @Override + public ReactiveCouchbaseResourceHolder getTransactionResources(TransactionOptions options, + CoreTransactionAttemptContext atr) { + ReactiveCouchbaseResourceHolder holder = delegate.getTransactionResources(options, atr); + return holder; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#withSession(com.mongodb.session.CoreTransactionAttemptContext) + */ + @Override + public ReactiveCouchbaseClientFactory withCore(ReactiveCouchbaseResourceHolder core) { + return delegate.withCore(core); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#isTransactionActive() + */ + @Override + public boolean isTransactionActive() { + return transactionResources != null && transactionResources.hasActiveTransaction(); + } + + @Override + public CouchbaseTransactionalOperator getTransactionalOperator() { + return delegate.getTransactionalOperator(); + } + + @Override + public ReactiveCouchbaseClientFactory with(CouchbaseTransactionalOperator txOp) { + return delegate.with(txOp); + } + + private ClusterInterface decorateDatabase(ClusterInterface database) { + return createProxyInstance(transactionResources, database, ClusterInterface.class); + } + + private ClusterInterface proxyDatabase(ReactiveCouchbaseResourceHolder session, ClusterInterface database) { + return createProxyInstance(session, database, ClusterInterface.class); + } + + private Collection proxyCollection(ReactiveCouchbaseResourceHolder session, Collection collection) { + return createProxyInstance(session, collection, Collection.class); + } + + private T createProxyInstance(ReactiveCouchbaseResourceHolder session, T target, Class targetType) { + + ProxyFactory factory = new ProxyFactory(); + factory.setTarget(target); + factory.setInterfaces(targetType); + factory.setOpaque(true); + + factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, ReactiveCouchbaseResourceHolder.class, + ClusterInterface.class, this::proxyDatabase, Collection.class, this::proxyCollection)); + + return targetType.cast(factory.getProxy(target.getClass().getClassLoader())); + } + + public ReactiveCouchbaseResourceHolder getTransactionResources() { + return this.transactionResources; + } + + public ReactiveCouchbaseClientFactory getDelegate() { + return this.delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + CoreTransactionAttemptContextBoundCouchbaseClientFactory that = (CoreTransactionAttemptContextBoundCouchbaseClientFactory) o; + + if (!ObjectUtils.nullSafeEquals(this.transactionResources, that.transactionResources)) { + return false; + } + return ObjectUtils.nullSafeEquals(this.delegate, that.delegate); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(this.transactionResources); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.delegate); + return result; + } + + public String toString() { + return "SimpleReactiveCouchbaseDatabaseFactory.CoreTransactionAttemptContextBoundCouchDbFactory(session=" + + this.getTransactionResources() + ", delegate=" + this.getDelegate() + ")"; + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 6982d64f4..80f4e72b9 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -17,19 +17,31 @@ package org.springframework.data.couchbase.config; import static com.couchbase.client.java.ClusterOptions.clusterOptions; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_MAPPING_CONTEXT; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TRANSACTIONS; +import java.time.Duration; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.transaction.config.CoreTransactionsConfig; +import com.couchbase.client.java.query.QueryScanConsistency; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; +import org.springframework.data.couchbase.SimpleReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; @@ -40,12 +52,22 @@ import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.interceptor.CouchbaseTransactionInterceptor; import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import com.couchbase.client.core.cnc.Event; import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature; import com.couchbase.client.core.encryption.CryptoManager; import com.couchbase.client.core.env.Authenticator; @@ -57,7 +79,6 @@ import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.json.JacksonTransformers; import com.couchbase.client.java.json.JsonValueModule; -import com.couchbase.client.java.query.QueryScanConsistency; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -123,15 +144,21 @@ protected Authenticator authenticator() { * @param couchbaseCluster the cluster reference from the SDK. * @return the initialized factory. */ - @Bean + @Bean(name = BeanNames.COUCHBASE_CLIENT_FACTORY) public CouchbaseClientFactory couchbaseClientFactory(final Cluster couchbaseCluster) { return new SimpleCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); } + @Bean + public ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory(final Cluster couchbaseCluster) { + return new SimpleReactiveCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName()); + } + @Bean(destroyMethod = "disconnect") public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment) { - return Cluster.connect(getConnectionString(), + Cluster c = Cluster.connect(getConnectionString(), clusterOptions(authenticator()).environment(couchbaseClusterEnvironment)); + return c; } @Bean(destroyMethod = "shutdown") @@ -141,6 +168,9 @@ 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); return builder.build(); } @@ -156,26 +186,31 @@ protected void configureEnvironment(final ClusterEnvironment.Builder builder) { @Bean(name = BeanNames.COUCHBASE_TEMPLATE) public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, - getDefaultConsistency()); + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { + return new CouchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, + couchbaseTranslationService, getDefaultConsistency()); } public CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { - return couchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return couchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, + new JacksonTranslationService()); } @Bean(name = BeanNames.REACTIVE_COUCHBASE_TEMPLATE) - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter, TranslationService couchbaseTranslationService) { - return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, couchbaseTranslationService, - getDefaultConsistency()); + return new ReactiveCouchbaseTemplate(reactiveCouchbaseClientFactory, mappingCouchbaseConverter, + couchbaseTranslationService, getDefaultConsistency()); } - public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate( + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, MappingCouchbaseConverter mappingCouchbaseConverter) { - return reactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, + return reactiveCouchbaseTemplate( reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService()); } @@ -257,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; @@ -282,7 +317,7 @@ public TranslationService couchbaseTranslationService() { * Creates a {@link CouchbaseMappingContext} equipped with entity classes scanned from the mapping base package. * */ - @Bean + @Bean(COUCHBASE_MAPPING_CONTEXT) public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customConversions) throws Exception { CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); mappingContext.setInitialEntitySet(getInitialEntitySet()); @@ -310,6 +345,55 @@ public ObjectMapper couchbaseObjectMapper() { return mapper; } + /***** ALL THIS TX SHOULD BE MOVED OUT INTO THE IMPL OF AbstractCouchbaseConfiguration *****/ + + @Bean(BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + ReactiveCouchbaseTransactionManager reactiveTransactionManager( + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) { + return new ReactiveCouchbaseTransactionManager(reactiveCouchbaseClientFactory); + } + +// @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) +// CouchbaseTransactionManager transactionManager(CouchbaseClientFactory couchbaseClientFactory) { +// return new CouchbaseTransactionManager(couchbaseClientFactory); +// } + + // todo gp experimenting with making CouchbaseSimpleCallbackTransactionManager the default - but it doesn't play + // nice with MR's changes to insert CouchbaseTransactionInterceptor + // todo mr THIS DOES NOT WORK WELL with @TestTransaction / @BeforeTransaction / @AfterTransaction etc. + // todo mr Maybe it is only useful with @Transactional? + @Bean(BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + CouchbaseSimpleCallbackTransactionManager callbackTransactionManager(ReactiveCouchbaseClientFactory clientFactory, TransactionOptions options) { + return new CouchbaseSimpleCallbackTransactionManager(clientFactory, options); + } + + @Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER) + CouchbaseTransactionManager transactionManager(CouchbaseClientFactory clientFactory, TransactionOptions options) { + return new CouchbaseTransactionManager(clientFactory, options); + } + + // 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 + } + + /** + * Blocking Transaction Manager + * + * @param couchbaseTemplate + * @return + */ +// @Bean(BeanNames.COUCHBASE_CALLBACK_TRANSACTION_MANAGER) +// CouchbaseCallbackTransactionManager callbackTransactionManager(CouchbaseTemplate couchbaseTemplate, ReactiveCouchbaseTemplate couchbaseReactiveTemplate) { +// return new CouchbaseCallbackTransactionManager(couchbaseTemplate, couchbaseReactiveTemplate); +// } + /** * Configure whether to automatically create indices for domain types by deriving the from the entity or not. */ @@ -375,5 +459,33 @@ private boolean nonShadowedJacksonPresent() { public QueryScanConsistency getDefaultConsistency() { return null; } +/* + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) { + TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(); + interceptor.setTransactionAttributeSource(transactionAttributeSource); + return interceptor; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionAttributeSource transactionAttributeSource() { + return new AnnotationTransactionAttributeSource(); + } + @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( + TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { + + BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); + advisor.setTransactionAttributeSource(transactionAttributeSource); + advisor.setAdvice(transactionInterceptor); + // if (this.enableTx != null) { + // advisor.setOrder(this.enableTx.getNumber("order")); + // } + return advisor; + } + */ } diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index cb9bf63ea..100c841e5 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -34,6 +34,8 @@ public class BeanNames { public static final String COUCHBASE_CUSTOM_CONVERSIONS = "couchbaseCustomConversions"; + public static final String COUCHBASE_TRANSACTIONS = "couchbaseTransactions"; + /** * The name for the bean that stores custom mapping between repositories and their backing couchbaseOperations. */ @@ -59,4 +61,12 @@ public class BeanNames { * The name for the bean that will handle reactive audit trail marking of entities. */ public static final String REACTIVE_COUCHBASE_AUDITING_HANDLER = "reactiveCouchbaseAuditingHandler"; + + public static final String COUCHBASE_CLIENT_FACTORY = "couchbaseClientFactory"; + + public static final String REACTIVE_COUCHBASE_TRANSACTION_MANAGER = "reactiveCouchbaseTransactionManager"; + + public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager"; + + public static final String COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER = "couchbaseSimpleCallbackTransactionManager"; } diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java new file mode 100644 index 000000000..8164f2cd2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -0,0 +1,226 @@ +/* + * Copyright 2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; +import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; + +import com.couchbase.client.core.error.CouchbaseException; +import org.springframework.util.ClassUtils; + +import java.util.Map; +import java.util.Set; + +public abstract class AbstractTemplateSupport { + + final ReactiveCouchbaseTemplate template; + final CouchbaseConverter converter; + final MappingContext, CouchbasePersistentProperty> mappingContext; + final TranslationService translationService; + ApplicationContext applicationContext; + static final Logger LOG = LoggerFactory.getLogger(AbstractTemplateSupport.class); + + public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConverter converter, TranslationService translationService) { + this.template = template; + this.converter = converter; + this.mappingContext = converter.getMappingContext(); + this.translationService = translationService; + } + + abstract ReactiveCouchbaseTemplate getReactiveTemplate(); + + public T decodeEntityBase(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + final CouchbaseDocument converted = new CouchbaseDocument(id); + converted.setId(id); + + // this is the entity class defined for the repository. It may not be the class of the document that was read + // we will reset it after reading the document + // + // This will fail for the case where: + // 1) The version is defined in the concrete class, but not in the abstract class; and + // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in + // the source is null. + // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, + // but that is a lot of work to do every time just for this very rare and avoidable case. + // TypeInformation typeToUse = typeMapper.readType(source, type); + + CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); + + if (persistentEntity == null) { // method could return a Long, Boolean, String etc. + // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left + // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a + // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. + // if this is a Collection or array, only the first element will be returned. + Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) + .getContent().entrySet(); + return (T) set.iterator().next().getValue(); + } + + // if possible, set the version property in the source so that if the constructor has a long version argument, + // it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure + // can be avoid by defining the argument as Long instead of long. + // persistentEntity is still the (possibly abstract) class specified in the repository definition + // it's possible that the abstract class does not have a version property, and this won't be able to set the version + if (cas != 0 && persistentEntity.getVersionProperty() != null) { + converted.put(persistentEntity.getVersionProperty().getName(), cas); + } + + // if the constructor has an argument that is long version, then construction will fail if the 'version' + // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. + // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) + T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); + final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); + + persistentEntity = couldBePersistentEntity(readEntity.getClass()); + + if (cas != 0 && persistentEntity.getVersionProperty() != null) { + accessor.setProperty(persistentEntity.getVersionProperty(), cas); + } + if (persistentEntity.transactionResultProperty() != null) { + accessor.setProperty(persistentEntity.transactionResultProperty(), System.identityHashCode(txResultHolder)); + } + N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id, scope, collection); + + if(holder != null){ + holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + } + + return accessor.getBean(); + } + + CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { + if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { + return null; + } + return mappingContext.getPersistentEntity(entityClass); + } + + + + public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + + final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() + .getRequiredPersistentEntity(entity.getClass()); + + final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + if (idProperty != null) { + accessor.setProperty(idProperty, id); + } + + final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + if (versionProperty != null) { + accessor.setProperty(versionProperty, cas); + } + + final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty(); + if (transactionResultProperty != null) { + accessor.setProperty(transactionResultProperty, System.identityHashCode(txResultHolder)); + } + if(holder != null){ + holder.transactionResultHolder(txResultHolder, (T)accessor.getBean()); + } + maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted)); + return (T) accessor.getBean(); + + } + + public Long getCas(final Object entity) { + final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + long cas = 0; + if (versionProperty != null) { + Object casObject = accessor.getProperty(versionProperty); + if (casObject instanceof Number) { + cas = ((Number) casObject).longValue(); + } + } + return cas; + } + + public Object getId(final Object entity) { + final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + Object id = null; + if (idProperty != null) { + id = accessor.getProperty(idProperty); + } + return id; + } + + public String getJavaNameForEntity(final Class clazz) { + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); + MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); + return info.getJavaType().getName(); + } + + ConvertingPropertyAccessor getPropertyAccessor(final T source) { + CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); + return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); + } + + public Integer getTxResultKey(T source) { + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(source.getClass()); + final CouchbasePersistentProperty transactionResultProperty = persistentEntity.transactionResultProperty(); + if (transactionResultProperty == null) { + throw new CouchbaseException("the entity class " + source.getClass() + + " does not have a property required for transactions:\n\t@TransactionResult TransactionResultHolder txResultHolder"); + } + return getPropertyAccessor(source).getProperty(transactionResultProperty, Integer.class); + } + + public void maybeEmitEvent(CouchbaseMappingEvent event) { + if (canPublishEvent()) { + try { + this.applicationContext.publishEvent(event); + } catch (Exception e) { + LOG.warn("{} thrown during {}", e, event); + throw e; + } + } else { + LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext"); + } + + } + + private boolean canPublishEvent() { + return this.applicationContext != null; + } + + public TranslationService getTranslationService(){ + return translationService; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index 33ce0791a..8cec31313 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -18,6 +18,9 @@ import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; + +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; import com.couchbase.client.java.query.QueryScanConsistency; @@ -50,4 +53,8 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations { * Returns the default consistency to use for queries */ QueryScanConsistency getConsistency(); + T save(T entity); + + Long count(Query query, Class domainType); + } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 4bf781210..322bf4f73 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -16,11 +16,14 @@ package org.springframework.data.couchbase.core; +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; @@ -28,6 +31,8 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -49,26 +54,31 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex private final CouchbaseTemplateSupport templateSupport; private final MappingContext, CouchbasePersistentProperty> mappingContext; private final ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + private final QueryScanConsistency scanConsistency; private @Nullable CouchbasePersistentEntityIndexCreator indexCreator; - private QueryScanConsistency scanConsistency; + private CouchbaseTransactionalOperator couchbaseTransactionalOperator; - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { - this(clientFactory, converter, new JacksonTranslationService()); + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter) { + this(clientFactory, reactiveCouchbaseClientFactory, converter, new JacksonTranslationService()); } - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService) { - this(clientFactory, converter, translationService, null); + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, CouchbaseConverter converter, + final TranslationService translationService) { + this(clientFactory, reactiveCouchbaseClientFactory, converter, translationService, null); } - public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService, QueryScanConsistency scanConsistency) { + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, + final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, final CouchbaseConverter converter, + final TranslationService translationService, QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.templateSupport = new CouchbaseTemplateSupport(this, converter, translationService); - this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService, - scanConsistency); + this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(reactiveCouchbaseClientFactory, converter, + translationService, scanConsistency); this.scanConsistency = scanConsistency; + this.mappingContext = this.converter.getMappingContext(); if (mappingContext instanceof CouchbaseMappingContext) { CouchbaseMappingContext cmc = (CouchbaseMappingContext) mappingContext; @@ -78,6 +88,20 @@ public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final Couch } } + public T save(T entity) { + if (hasNonZeroVersionProperty(entity, templateSupport.converter)) { + return replaceById((Class) entity.getClass()).one(entity); + //} else if (getTransactionalOperator() != null) { + // return insertById((Class) entity.getClass()).one(entity); + } else { + return upsertById((Class) entity.getClass()).one(entity); + } + } + + public Long count(Query query, Class domainType) { + return findByQuery(domainType).matching(query).count(); + } + @Override public ExecutableUpsertById upsertById(final Class domainType) { return new ExecutableUpsertByIdOperationSupport(this).upsertById(domainType); @@ -210,4 +234,29 @@ public TemplateSupport support() { return templateSupport; } + public CouchbaseTemplate with(CouchbaseTransactionalOperator couchbaseTransactionalOperator) { + this.couchbaseTransactionalOperator = couchbaseTransactionalOperator; + return this; + } + + /** + * Get the TransactionalOperator from
+ * 1. The template.clientFactory
+ * 2. The template.threadLocal
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() + *//* + private CouchbaseStuffHandle getTransactionalOperator() { + if (this.getCouchbaseClientFactory().getTransactionalOperator() != null) { + return this.getCouchbaseClientFactory().getTransactionalOperator(); + } + ReactiveCouchbaseTemplate t = this.reactive(); + PseudoArgs pArgs = t.getPseudoArgs(); + if (pArgs != null && pArgs.getTxOp() != null) { + return pArgs.getTxOp(); + } + return null; + } + */ } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index b86806abb..7ce71a517 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -16,33 +16,20 @@ package org.springframework.data.couchbase.core; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertCallback; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; -import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; -import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import org.springframework.data.mapping.callback.EntityCallbacks; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Internal encode/decode support for CouchbaseTemplate. @@ -53,23 +40,15 @@ * @author Carlos Espinaco * @since 3.0 */ -class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSupport { - - private static final Logger LOG = LoggerFactory.getLogger(CouchbaseTemplateSupport.class); +class CouchbaseTemplateSupport extends AbstractTemplateSupport implements ApplicationContextAware, TemplateSupport { private final CouchbaseTemplate template; - private final CouchbaseConverter converter; - private final MappingContext, CouchbasePersistentProperty> mappingContext; - private final TranslationService translationService; private EntityCallbacks entityCallbacks; - private ApplicationContext applicationContext; public CouchbaseTemplateSupport(final CouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { + super(template.reactive(), converter, translationService); this.template = template; - this.converter = converter; - this.mappingContext = converter.getMappingContext(); - this.translationService = translationService; } @Override @@ -84,123 +63,39 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) { } @Override - public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection) { - final CouchbaseDocument converted = new CouchbaseDocument(id); - converted.setId(id); - - // this is the entity class defined for the repository. It may not be the class of the document that was read - // we will reset it after reading the document - // - // This will fail for the case where: - // 1) The version is defined in the concrete class, but not in the abstract class; and - // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in - // the source is null. - // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, - // but that is a lot of work to do every time just for this very rare and avoidable case. - // TypeInformation typeToUse = typeMapper.readType(source, type); - - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { // method could return a Long, Boolean, String etc. - // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left - // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a - // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. - // if this is a Collection or array, only the first element will be returned. - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - - // if possible, set the version property in the source so that if the constructor has a long version argument, - // it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure - // can be avoid by defining the argument as Long instead of long. - // persistentEntity is still the (possibly abstract) class specified in the repository definition - // it's possible that the abstract class does not have a version property, and this won't be able to set the version - if (cas != 0 && persistentEntity.getVersionProperty() != null) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); - } - - // if the constructor has an argument that is long version, then construction will fail if the 'version' - // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. - // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); - final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - - persistentEntity = couldBePersistentEntity(readEntity.getClass()); - - if (cas != 0 && persistentEntity.getVersionProperty() != null) { - accessor.setProperty(persistentEntity.getVersionProperty(), cas); - } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection); - return accessor.getBean(); + ReactiveCouchbaseTemplate getReactiveTemplate() { + return template.reactive(); } - CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { - if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { - return null; - } - return mappingContext.getPersistentEntity(entityClass); + @Override + public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txHolder); } @Override - public Object applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { - Object returnValue; - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - if (versionProperty != null) { - accessor.setProperty(versionProperty, cas); - returnValue = accessor.getBean(); - } else { - returnValue = entity; - } - maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); - - return returnValue; + public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txHolder, ReactiveCouchbaseResourceHolder holder) { + return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder); } @Override - public Object applyUpdatedId(final Object entity, Object id) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); - - if (idProperty != null) { - accessor.setProperty(idProperty, id); - return accessor.getBean(); - } - return entity; + public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder) { + return applyResult(entity, converted, id, cas,txResultHolder, null); } @Override - public long getCas(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - long cas = 0; - if (versionProperty != null) { - Object casObject = accessor.getProperty(versionProperty); - if (casObject instanceof Number) { - cas = ((Number) casObject).longValue(); - } - } - return cas; + public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return applyResultBase(entity, converted, id, cas, txResultHolder, holder); } @Override - public String getJavaNameForEntity(final Class clazz) { - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); - MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); - return info.getJavaType().getName(); + public Integer getTxResultHolder(T source) { + return null; } - private ConvertingPropertyAccessor getPropertyAccessor(final T source) { - CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); - } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { @@ -225,24 +120,6 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; } - public void maybeEmitEvent(CouchbaseMappingEvent event) { - if (canPublishEvent()) { - try { - this.applicationContext.publishEvent(event); - } catch (Exception e) { - LOG.warn("{} thrown during {}", e, event); - throw e; - } - } else { - LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext"); - } - - } - - private boolean canPublishEvent() { - return this.applicationContext != null; - } - protected T maybeCallBeforeConvert(T object, String collection) { if (entityCallbacks != null) { return entityCallbacks.callback(BeforeConvertCallback.class, object, collection); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java index 2aed295fb..58e9146ea 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java @@ -37,7 +37,8 @@ * * @author Christoph Strobl * @since 2.0 - */public interface ExecutableFindByAnalyticsOperation { + */ +public interface ExecutableFindByAnalyticsOperation { /** * Queries the analytics service. @@ -114,65 +115,23 @@ default Optional first() { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { - - /** - * Set the filter for the analytics query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - TerminatingFindByAnalytics matching(AnalyticsQuery query); - - } - /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * * @param options to use for execution */ @Override - FindByAnalyticsWithQuery withOptions(AnalyticsOptions options); - } - - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithOptions inCollection(String collection); - } - - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { - /** - * With a different scope - * - * @param scope the scope to use. - */ - @Override - FindByAnalyticsInCollection inScope(String scope); + TerminatingFindByAnalytics withOptions(AnalyticsOptions options); } @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { + interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { /** * Allows to override the default scan consistency. @@ -180,7 +139,7 @@ interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { * @param scanConsistency the custom scan consistency to use for this analytics query. */ @Deprecated - FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); + FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); } @@ -194,10 +153,22 @@ interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWit FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); } + interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { + + /** + * Set the filter for the analytics query to be used. + * + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + FindByAnalyticsWithConsistency matching(AnalyticsQuery query); + + } + /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { /** * Define the target type fields should be mapped to.
@@ -207,9 +178,39 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistenc * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithConsistency as(Class returnType); + FindByAnalyticsWithQuery as(Class returnType); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByAnalyticsWithProjection inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } - interface ExecutableFindByAnalytics extends FindByAnalyticsWithProjection {} + interface ExecutableFindByAnalytics extends FindByAnalyticsInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java index ad88bc4de..c90fe410a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -83,7 +83,7 @@ public List all() { } @Override - public TerminatingFindByAnalytics matching(final AnalyticsQuery query) { + public FindByAnalyticsWithConsistency matching(final AnalyticsQuery query) { return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); } @@ -102,7 +102,7 @@ public FindByAnalyticsInCollection inScope(final String scope) { } @Override - public FindByAnalyticsWithConsistency inCollection(final String collection) { + public FindByAnalyticsWithProjection inCollection(final String collection) { return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); } @@ -121,7 +121,7 @@ public FindByAnalyticsWithConsistency withConsistency(final AnalyticsScanCons } @Override - public FindByAnalyticsWithConsistency as(final Class returnType) { + public FindByAnalyticsWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index 9cb60eeb7..2cb6def40 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -18,14 +18,16 @@ import java.time.Duration; import java.util.Collection; -import org.springframework.data.couchbase.core.support.OneAndAllId; import org.springframework.data.couchbase.core.support.InCollection; +import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; -import org.springframework.data.couchbase.core.support.InScope; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; -import org.springframework.data.couchbase.core.support.WithExpiry; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Get Operations @@ -82,19 +84,58 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } + interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + @Override + FindByIdWithOptions project(String... fields); + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + + /** + * Provide attempt context + * + * @param the entity type to use for the results + */ + interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { + /** + * Finds the distinct values for a specified {@literal field} across a single collection + * + * @param txCtx Must not be {@literal null}. + * @return new instance of {@link ExecutableFindById}. + * @throws IllegalArgumentException if field is {@literal null}. + */ + @Override + FindByIdWithProjection transaction(CouchbaseTransactionalOperator txCtx); + } + + interface FindByIdTxOrNot extends FindByIdWithExpiry, FindByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdWithOptions, InCollection { + interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdWithOptions inCollection(String collection); + FindByIdTxOrNot inCollection(String collection); } /** @@ -112,31 +153,11 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } - interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - @Override - FindByIdInScope project(String... fields); - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type to use for the results */ - interface ExecutableFindById extends FindByIdWithExpiry {} + interface ExecutableFindById extends FindByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 38cf2716b..e17596292 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -21,6 +21,7 @@ import java.util.List; import org.springframework.data.couchbase.core.ReactiveFindByIdOperationSupport.ReactiveFindByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.java.kv.GetOptions; @@ -35,7 +36,7 @@ public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOpe @Override public ExecutableFindById findById(Class domainType) { - return new ExecutableFindByIdSupport<>(template, domainType, null, null, null, null, null); + return new ExecutableFindByIdSupport<>(template, domainType, null, null, null, null, null, null); } static class ExecutableFindByIdSupport implements ExecutableFindById { @@ -47,10 +48,11 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final GetOptions options; private final List fields; private final Duration expiry; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String scope, String collection, - GetOptions options, List fields, Duration expiry) { + GetOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -58,13 +60,16 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { this.options = options; this.fields = fields; this.expiry = expiry; + this.txCtx = txCtx; this.reactiveSupport = new ReactiveFindByIdSupport<>(template.reactive(), domainType, scope, collection, options, - fields, expiry, new NonReactiveSupportWrapper(template.support())); + fields, expiry, txCtx, new NonReactiveSupportWrapper(template.support())); } @Override public T one(final String id) { - return reactiveSupport.one(id).block(); + //Mono.deferContextual(ctx -> { System.err.println("ExecutableFindById.ctx: "+ctx); return Mono.empty();}).block(); + return reactiveSupport.one(id)/*.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder())*/.block(); } @Override @@ -75,29 +80,35 @@ public Collection all(final Collection ids) { @Override public TerminatingFindById withOptions(final GetOptions options) { Assert.notNull(options, "Options must not be null."); - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } @Override - public FindByIdWithOptions inCollection(final String collection) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); + public FindByIdTxOrNot inCollection(final String collection) { + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } @Override public FindByIdInCollection inScope(final String scope) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } @Override public FindByIdInScope project(String... fields) { Assert.notEmpty(fields, "Fields must not be null."); - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), + expiry, txCtx); } @Override public FindByIdWithProjection withExpiry(final Duration expiry) { - return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, - expiry); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); + } + + @Override + public FindByIdWithExpiry transaction(CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index f147dda67..f6219accb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -29,6 +29,8 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.lang.Nullable; import com.couchbase.client.java.query.QueryOptions; @@ -128,42 +130,12 @@ default Optional first() { } - /** - * Fluent methods to specify the query - * - * @param the entity type to use for the results. - */ - interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { - - /** - * Set the filter for the query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - @Override - TerminatingFindByQuery matching(Query query); - - /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. - * - * @param criteria must not be {@literal null}. - * @return new instance of {@link ExecutableFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. - */ - @Override - default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - - } - /** * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { + interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { /** * Fluent method to specify options to use for execution * @@ -174,66 +146,86 @@ interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOp } /** - * Fluent method to specify the collection. + * To be removed at the next major release. use WithConsistency instead * * @param the entity type to use for the results. */ - interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryWithOptions { + /** - * With a different collection + * Allows to override the default scan consistency. * - * @param collection the collection to use. + * @param scanConsistency the custom scan consistency to use for this query. */ - @Override - FindByQueryWithOptions inCollection(String collection); + @Deprecated + FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify the scope. + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. * * @param the entity type to use for the results. */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + /** - * With a different scope + * Allows to override the default scan consistency. * - * @param scope the scope to use. + * @param scanConsistency the custom scan consistency to use for this query. */ @Override - FindByQueryInCollection inScope(String scope); + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } /** - * To be removed at the next major release. use WithConsistency instead - * + * Fluent method to specify transaction + * * @param the entity type to use for the results. */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryInScope { + interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { /** - * Allows to override the default scan consistency. + * Finds the distinct values for a specified {@literal field} across a single collection * - * @param scanConsistency the custom scan consistency to use for this query. + * @param txCtx Must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. */ - @Deprecated - FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); + @Override + TerminatingFindByQuery transaction(CouchbaseTransactionalOperator txCtx); } + interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} + /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * Fluent methods to specify the query * * @param the entity type to use for the results. */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { /** - * Allows to override the default scan consistency. + * Set the filter for the query to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ @Override - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + FindByQueryTxOrNot matching(Query query); + + /** + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * + * @param criteria must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. + */ + @Override + default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** @@ -241,7 +233,7 @@ interface FindByQueryWithConsistency extends FindByQueryConsistentWith, Wi * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithConsistency { + interface FindByQueryWithProjection extends FindByQueryWithQuery { /** * Define the target type fields should be mapped to.
@@ -251,7 +243,7 @@ interface FindByQueryWithProjection extends FindByQueryWithConsistency { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithConsistency as(Class returnType); + FindByQueryWithQuery as(Class returnType); } /** @@ -287,7 +279,37 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @throws IllegalArgumentException if field is {@literal null}. */ @Override - FindByQueryWithProjection distinct(String[] distinctFields); + FindByQueryWithProjecting distinct(String[] distinctFields); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByQueryWithDistinct inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByQueryInScope extends FindByQueryInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByQueryInCollection inScope(String scope); } /** @@ -295,6 +317,6 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * * @param the entity type to use for the results */ - interface ExecutableFindByQuery extends FindByQueryWithDistinct {} + interface ExecutableFindByQuery extends FindByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index cffdd5716..f793c8b94 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -20,6 +20,7 @@ import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; @@ -44,7 +45,7 @@ public ExecutableFindByQueryOperationSupport(final CouchbaseTemplate template) { @Override public ExecutableFindByQuery findByQuery(final Class domainType) { return new ExecutableFindByQuerySupport(template, domainType, domainType, ALL_QUERY, null, null, null, null, - null, null); + null, null, null); } static class ExecutableFindByQuerySupport implements ExecutableFindByQuery { @@ -60,16 +61,18 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final QueryOptions options; private final String[] distinctFields; private final String[] fields; + private final 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 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; this.query = query; this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, returnType, query, - scanConsistency, scope, collection, options, distinctFields, fields, + scanConsistency, scope, collection, options, distinctFields, fields, txCtx, new NonReactiveSupportWrapper(template.support())); this.scanConsistency = scanConsistency; this.scope = scope; @@ -77,6 +80,7 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery this.options = options; this.distinctFields = distinctFields; this.fields = fields; + this.txCtx = txCtx; } @Override @@ -95,7 +99,7 @@ public List all() { } @Override - public TerminatingFindByQuery matching(final Query query) { + public FindByQueryTxOrNot matching(final Query query) { QueryScanConsistency scanCons; if (query.getScanConsistency() != null) { scanCons = query.getScanConsistency(); @@ -103,27 +107,27 @@ public TerminatingFindByQuery matching(final Query query) { scanCons = scanConsistency; } return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields, fields); + options, distinctFields, fields, txCtx); } @Override @Deprecated public FindByQueryInScope consistentWith(final QueryScanConsistency scanConsistency) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override public FindByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override - public FindByQueryWithConsistency as(final Class returnType) { + public FindByQueryWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override @@ -131,11 +135,11 @@ public FindByQueryWithProjection project(String[] fields) { Assert.notNull(fields, "Fields must not be null"); Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override - public FindByQueryWithProjection distinct(final String[] distinctFields) { + public FindByQueryWithProjecting distinct(final String[] distinctFields) { Assert.notNull(distinctFields, "distinctFields must not be null"); Assert.isNull(fields, "only one of project(fields) and distinct(distinctFields) can be specified"); // Coming from an annotation, this cannot be null. @@ -143,7 +147,14 @@ public FindByQueryWithProjection distinct(final String[] distinctFields) { // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields, fields); + collection, options, dFields, fields, txCtx); + } + + @Override + public FindByQueryWithDistinct transaction(CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null!"); + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields, txCtx); } @Override @@ -169,19 +180,19 @@ public boolean exists() { public TerminatingFindByQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override public FindByQueryInCollection inScope(final String scope) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } @Override - public FindByQueryWithConsistency inCollection(final String collection) { + public FindByQueryWithDistinct inCollection(final String collection) { return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields); + collection, options, distinctFields, fields, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index 0465d5022..aa8f06caf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -24,11 +24,13 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Insert Operations @@ -73,8 +75,7 @@ interface TerminatingInsertById extends OneAndAllEntity { * * @param the entity type to use. */ - interface InsertByIdWithOptions - extends TerminatingInsertById, WithInsertOptions { + interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertOptions { /** * Fluent method to specify options to use for execution. * @@ -84,19 +85,42 @@ interface InsertByIdWithOptions TerminatingInsertById withOptions(InsertOptions options); } + interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { + + @Override + InsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + + interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { + @Override + InsertByIdWithExpiry transaction(CouchbaseTransactionalOperator txCtx); + } + + interface InsertByIdTxOrNot extends InsertByIdWithExpiry, InsertByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { + interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdWithOptions inCollection(String collection); + InsertByIdTxOrNot inCollection(String collection); } /** @@ -114,27 +138,11 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } - interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { - - @Override - InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ExecutableInsertById extends InsertByIdWithExpiry {} + interface ExecutableInsertById extends InsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index 8eb4b99f5..2fc06a1d2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -19,6 +19,7 @@ import java.util.Collection; import org.springframework.data.couchbase.core.ReactiveInsertByIdOperationSupport.ReactiveInsertByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -38,7 +39,7 @@ public ExecutableInsertByIdOperationSupport(final CouchbaseTemplate template) { public ExecutableInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableInsertByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ExecutableInsertByIdSupport implements ExecutableInsertById { @@ -52,11 +53,12 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final 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; @@ -66,8 +68,10 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; this.reactiveSupport = new ReactiveInsertByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, + new NonReactiveSupportWrapper(template.support())); } @Override @@ -84,26 +88,26 @@ public Collection all(Collection objects) { public TerminatingInsertById withOptions(final InsertOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override public InsertByIdInCollection inScope(final String scope) { return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override - public InsertByIdWithOptions inCollection(final String collection) { + public InsertByIdTxOrNot inCollection(final String collection) { return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override @@ -111,14 +115,21 @@ public InsertByIdInScope withDurability(final PersistTo persistTo, final Repl Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); } @Override public InsertByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry); + durabilityLevel, expiry, txCtx); + } + + @Override + public InsertByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 02381669e..ffb916a89 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -21,13 +21,16 @@ import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithCas; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Remove Operations on KV service. @@ -61,6 +64,14 @@ interface TerminatingRemoveById extends OneAndAllId { @Override RemoveResult one(String id); + /** + * Remove one document based on the entity. Transactions need the entity for the cas. + * + * @param entity the document ID. + * @return result of the remove + */ + RemoveResult oneEntity(Object entity); + /** * Remove the documents in the collection. * @@ -70,6 +81,14 @@ interface TerminatingRemoveById extends OneAndAllId { @Override List all(Collection ids); + /** + * Remove documents based on the entities. Transactions need the entity for the cas. + * + * @param entities to remove. + * @return result of the remove + */ + List allEntities(Collection entities); + } /** @@ -85,17 +104,39 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } + interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { + + @Override + RemoveByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability, WithCas { + @Override + RemoveByIdWithDurability withCas(Long cas); + } + + interface RemoveByIdWithTransaction extends TerminatingRemoveById, WithTransaction { + @Override + TerminatingRemoveById transaction(CouchbaseTransactionalOperator txCtx); + } + + interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} + /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { + interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - RemoveByIdWithOptions inCollection(String collection); + RemoveByIdTxOrNot inCollection(String collection); } /** @@ -111,24 +152,9 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { RemoveByIdInCollection inScope(String scope); } - interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { - - @Override - RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability { - - RemoveByIdWithDurability withCas(Long cas); - } - /** * Provides methods for constructing remove operations in a fluent way. */ - interface ExecutableRemoveById extends RemoveByIdWithCas {} + interface ExecutableRemoveById extends RemoveByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index e0721ce5a..c5a9e34f4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -19,6 +19,7 @@ import java.util.List; import org.springframework.data.couchbase.core.ReactiveRemoveByIdOperationSupport.ReactiveRemoveByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -43,7 +44,7 @@ public ExecutableRemoveById removeById() { @Override public ExecutableRemoveById removeById(Class domainType) { return new ExecutableRemoveByIdSupport(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { @@ -57,11 +58,12 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Long cas) { + 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; @@ -71,8 +73,9 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdSupport(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, cas); + options, persistTo, replicateTo, durabilityLevel, cas, txCtx); this.cas = cas; + this.txCtx = txCtx; } @Override @@ -80,22 +83,33 @@ public RemoveResult one(final String id) { return reactiveRemoveByIdSupport.one(id).block(); } + @Override + public RemoveResult oneEntity(final Object entity) { + return reactiveRemoveByIdSupport.oneEntity(entity).block(); + } + @Override public List all(final Collection ids) { return reactiveRemoveByIdSupport.all(ids).collectList().block(); } @Override - public RemoveByIdWithOptions inCollection(final String collection) { + public List allEntities(final Collection entities) { + return reactiveRemoveByIdSupport.allEntities(entities).collectList().block(); + } + + + @Override + public RemoveByIdTxOrNot inCollection(final String collection) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override @@ -103,27 +117,34 @@ public RemoveByIdInScope withDurability(final PersistTo persistTo, final Replica Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public TerminatingRemoveById withOptions(final RemoveOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdInCollection inScope(final String scope) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdWithDurability withCas(Long cas) { return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); + } + + @Override + public RemoveByIdWithCas transaction(CouchbaseTransactionalOperator txCtx) { + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, cas, txCtx); } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index a6bfdf0cf..0fe7ed12c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -21,12 +21,13 @@ import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; -import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * RemoveBy Query Operations @@ -56,32 +57,58 @@ interface TerminatingRemoveByQuery { } /** - * Fluent methods to specify the query + * Fluent method to specify options. * - * @param the entity type. + * @param the entity type to use for the results. */ - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { + interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + TerminatingRemoveByQuery withOptions(QueryOptions options); + } - TerminatingRemoveByQuery matching(Query query); + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { - default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } + @Deprecated + RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith/*, WithConsistency */{ + //@Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify options. + * Fluent method to specify the transaction * * @param the entity type to use for the results. */ - interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { - /** - * Fluent method to specify options to use for execution - * - * @param options to use for execution - */ - RemoveByQueryWithQuery withOptions(QueryOptions options); + interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { + @Override + TerminatingRemoveByQuery transaction(CouchbaseTransactionalOperator txCtx); + } + + interface RemoveByQueryWithTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} + + /** + * Fluent methods to specify the query + * + * @param the entity type. + */ + interface RemoveByQueryWithQuery extends RemoveByQueryWithTxOrNot, WithQuery { + + RemoveByQueryWithTxOrNot matching(Query query); + + default RemoveByQueryWithTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + } /** @@ -89,13 +116,13 @@ interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQue * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithOptions inCollection(String collection); + RemoveByQueryWithQuery inCollection(String collection); } /** @@ -112,25 +139,11 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { - - @Deprecated - RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - @Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ExecutableRemoveByQuery extends RemoveByQueryWithConsistency {} + interface ExecutableRemoveByQuery extends RemoveByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index a97c62ba3..81007b7af 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -19,6 +19,7 @@ import org.springframework.data.couchbase.core.ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; @@ -36,7 +37,7 @@ public ExecutableRemoveByQueryOperationSupport(final CouchbaseTemplate template) @Override public ExecutableRemoveByQuery removeByQuery(Class domainType) { - return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null); + return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null, null); } static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuery { @@ -49,18 +50,21 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final String scope; private final String collection; private final QueryOptions options; + private final CouchbaseTransactionalOperator txCtx; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.query = query; this.reactiveSupport = new ReactiveRemoveByQuerySupport<>(template.reactive(), domainType, query, scanConsistency, - scope, collection, options); + scope, collection, options, txCtx); this.scanConsistency = scanConsistency; this.scope = scope; this.collection = collection; this.options = options; + this.txCtx = txCtx; } @Override @@ -69,42 +73,49 @@ public List all() { } @Override - public TerminatingRemoveByQuery matching(final Query query) { + public RemoveByQueryWithTxOrNot matching(final Query query) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override @Deprecated public RemoveByQueryInScope consistentWith(final QueryScanConsistency scanConsistency) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override - public RemoveByQueryWithConsistency inCollection(final String collection) { + public RemoveByQueryWithQuery inCollection(final String collection) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryInCollection inScope(final String scope) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } + + @Override + public TerminatingRemoveByQuery transaction(final CouchbaseTransactionalOperator txCtx) { + return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, + options, txCtx); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index 51ce8e98b..4aa8a39c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -24,11 +24,13 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Replace Operations @@ -83,19 +85,41 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } + interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { + @Override + ReplaceByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + ReplaceByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + @Override + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + + interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { + // 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); + } + + interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdWithOptions inCollection(String collection); + ReplaceByIdTxOrNot inCollection(String collection); } /** @@ -113,24 +137,11 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope { ReplaceByIdInCollection inScope(String scope); } - interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { - @Override - ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); - @Override - ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - @Override - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ExecutableReplaceById extends ReplaceByIdWithExpiry {} + interface ExecutableReplaceById extends ReplaceByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index ef47eb94a..751a5bf7a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -19,6 +19,7 @@ import java.util.Collection; import org.springframework.data.couchbase.core.ReactiveReplaceByIdOperationSupport.ReactiveReplaceByIdSupport; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -38,7 +39,7 @@ public ExecutableReplaceByIdOperationSupport(final CouchbaseTemplate template) { public ExecutableReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableReplaceByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ExecutableReplaceByIdSupport implements ExecutableReplaceById { @@ -52,11 +53,12 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, - final String collection, ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry) { + final 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; @@ -66,8 +68,10 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; this.reactiveSupport = new ReactiveReplaceByIdSupport<>(template.reactive(), domainType, scope, collection, - options, persistTo, replicateTo, durabilityLevel, expiry, new NonReactiveSupportWrapper(template.support())); + options, persistTo, replicateTo, durabilityLevel, expiry, txCtx, + new NonReactiveSupportWrapper(template.support())); } @Override @@ -81,16 +85,16 @@ public Collection all(Collection objects) { } @Override - public ReplaceByIdWithOptions inCollection(final String collection) { + public ReplaceByIdTxOrNot inCollection(final String collection) { return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override public ReplaceByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override @@ -98,27 +102,34 @@ public ReplaceByIdInScope withDurability(final PersistTo persistTo, final Rep Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override public ReplaceByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); + } + + @Override + public ReplaceByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, + replicateTo, durabilityLevel, expiry, txCtx); } @Override public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } @Override public ReplaceByIdInCollection inScope(final String scope) { return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, - replicateTo, durabilityLevel, expiry); + replicateTo, durabilityLevel, expiry, txCtx); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java index 0831f8eb4..56f93d02a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java @@ -83,19 +83,32 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } + interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { + @Override + UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithOptions inCollection(String collection); + UpsertByIdWithExpiry inCollection(String collection); } /** @@ -113,25 +126,11 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope { UpsertByIdInCollection inScope(String scope); } - interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { - @Override - UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ExecutableUpsertById extends UpsertByIdWithExpiry {} + interface ExecutableUpsertById extends UpsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java index 61b7a3945..7dd548b73 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java @@ -94,7 +94,7 @@ public UpsertByIdInCollection inScope(final String scope) { } @Override - public UpsertByIdWithOptions inCollection(final String collection) { + public UpsertByIdWithExpiry inCollection(final String collection) { return new ExecutableUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry); } diff --git a/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java b/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java new file mode 100644 index 000000000..adf8f0153 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/GenericSupport.java @@ -0,0 +1,64 @@ +package org.springframework.data.couchbase.core; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.lang.Nullable; + +import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.java.ReactiveCollection; + +// todo gp better name +@Stability.Internal +class GenericSupportHelper { + public final CouchbaseDocument converted; + public final Long cas; + public final ReactiveCollection collection; + public final @Nullable CoreTransactionAttemptContext ctx; + + public GenericSupportHelper(CouchbaseDocument doc, Long cas, ReactiveCollection collection, + @Nullable CoreTransactionAttemptContext ctx) { + this.converted = doc; + this.cas = cas; + this.collection = collection; + this.ctx = ctx; + } +} + +// todo gp better name +@Stability.Internal +public class GenericSupport { + public static Mono one(Mono tmpl, CouchbaseTransactionalOperator transactionalOperator, + String scopeName, String collectionName, ReactiveTemplateSupport support, T object, + Function> nonTransactional, Function> transactional) { + // todo gp how safe is this? I think we can switch threads potentially + // Optional ctxr = Optional.ofNullable((TransactionAttemptContext) + // org.springframework.transaction.support.TransactionSynchronizationManager.getResource(TransactionAttemptContext.class)); + + return tmpl.flatMap(template -> template.getCouchbaseClientFactory().withScope(scopeName) + .getCollection(collectionName).flatMap(collection -> support.encodeEntity(object) + .flatMap(converted -> tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + GenericSupportHelper gsh = new GenericSupportHelper(converted, support.getCas(object), + collection.reactive(), s.getCore() != null ? s.getCore() + : (transactionalOperator != null ? transactionalOperator.getAttemptContext() : null)); + if (gsh.ctx == null) { + System.err.println("non-tx"); + return nonTransactional.apply(gsh); + } else { + System.err.println("tx"); + return transactional.apply(gsh); + } + })).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + })))); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index bee19a4a8..2020a0d43 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -15,10 +15,12 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. @@ -40,33 +42,52 @@ public Mono encodeEntity(Object entityToEncode) { } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, - String collection) { - return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono applyUpdatedCas(Object entity, CouchbaseDocument converted, long cas) { - return Mono.fromSupplier(() -> support.applyUpdatedCas(entity, converted, cas)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } @Override - public Mono applyUpdatedId(Object entity, Object id) { - return Mono.fromSupplier(() -> support.applyUpdatedId(entity, id)); + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder) { + return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder)); } + @Override + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder, holder)); + } + + @Override public Long getCas(Object entity) { return support.getCas(entity); } + @Override + public Object getId(Object entity) { + return support.getId(entity); + } + @Override public String getJavaNameForEntity(Class clazz) { return support.getJavaNameForEntity(clazz); } @Override - public void maybeEmitEvent(CouchbaseMappingEvent event) { - support.maybeEmitEvent(event); + public Integer getTxResultHolder(T source) { + return support.getTxResultHolder(source); + } + + @Override + public TranslationService getTranslationService() { + return support.getTranslationService(); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java index 81b8cfdef..25d39e859 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java @@ -16,7 +16,10 @@ package org.springframework.data.couchbase.core; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; +import reactor.core.publisher.Mono; import com.couchbase.client.java.query.QueryScanConsistency; @@ -47,10 +50,14 @@ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOper /** * Returns the underlying client factory. */ - CouchbaseClientFactory getCouchbaseClientFactory(); + ReactiveCouchbaseClientFactory getCouchbaseClientFactory(); - /** - * @return the default consistency to use for queries - */ - QueryScanConsistency getConsistency(); + Mono save(T entity); + + Mono count(Query query, Class personClass); + + /** + * @return the default consistency to use for queries + */ + QueryScanConsistency getConsistency(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index a120b5280..4450da160 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -16,17 +16,35 @@ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; +import reactor.core.publisher.Mono; + +import java.util.function.Consumer; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; - +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseClientUtils; +import org.springframework.data.couchbase.transaction.SessionSynchronization; +import org.springframework.data.mapping.context.MappingContextEvent; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.ClusterInterface; import com.couchbase.client.java.Collection; import com.couchbase.client.java.query.QueryScanConsistency; @@ -40,24 +58,31 @@ */ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, ApplicationContextAware { - private final CouchbaseClientFactory clientFactory; + private final ReactiveCouchbaseClientFactory clientFactory; private final CouchbaseConverter converter; private final PersistenceExceptionTranslator exceptionTranslator; private final ReactiveCouchbaseTemplateSupport templateSupport; private ThreadLocal> threadLocalArgs = new ThreadLocal<>(); private QueryScanConsistency scanConsistency; - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { - this(clientFactory, converter, new JacksonTranslationService()); + public ReactiveCouchbaseTemplate with(CouchbaseTransactionalOperator txOp) { + // TODO: why does txOp go on the clientFactory? can't we just put it on the template?? + return new ReactiveCouchbaseTemplate(getCouchbaseClientFactory().with(txOp), getConverter(), + support().getTranslationService(), getConsistency()); } - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService) { - this(clientFactory, converter, translationService, null); + public CouchbaseTransactionalOperator txOperator() { + return clientFactory.getTransactionalOperator(); } - public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter, - final TranslationService translationService, QueryScanConsistency scanConsistency) { + public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, + final CouchbaseConverter converter) { + this(clientFactory, converter, new JacksonTranslationService(), null); + } + + public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, + final CouchbaseConverter converter, final TranslationService translationService, + final QueryScanConsistency scanConsistency) { this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); @@ -65,6 +90,49 @@ public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, fin this.scanConsistency = scanConsistency; } + // public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { + // this(clientFactory, converter, new JacksonTranslationService()); + // } + + // public ReactiveCouchbaseTemplate(final ReactiveCouchbaseClientFactory clientFactory, final CouchbaseConverter + // converter, + // final TranslationService translationService) { + // this.clientFactory = clientFactory; + // this.converter = converter; + // this.exceptionTranslator = this.clientFactory.getExceptionTranslator(); + // this.templateSupport = new ReactiveCouchbaseTemplateSupport(this, converter, translationService); + // } + + public Mono save(T entity) { + Assert.notNull(entity, "Entity must not be null!"); + Mono result; + final CouchbasePersistentEntity mapperEntity = getConverter().getMappingContext() + .getPersistentEntity(entity.getClass()); + final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty(); + final boolean versionPresent = versionProperty != null; + final Long version = versionProperty == null || versionProperty.getField() == null ? null + : (Long) ReflectionUtils.getField(versionProperty.getField(), entity); + final boolean existingDocument = version != null && version > 0; + + Class clazz = entity.getClass(); + + if (!versionPresent) { // the entity doesn't have a version property + // No version field - no cas + result = (Mono) upsertById(clazz).one(entity); + } else if (existingDocument) { // there is a version property, and it is non-zero + // Updating existing document with cas + result = (Mono) replaceById(clazz).one(entity); + } else { // there is a version property, but it's zero or not set. + // Creating new document + result = (Mono) insertById(clazz).one(entity); + } + return result; + } + + public Mono count(Query query, Class domainType) { + return findByQuery(domainType).matching(query).all().count(); + } + @Override public ReactiveFindById findById(Class domainType) { return new ReactiveFindByIdOperationSupport(this).findById(domainType); @@ -127,16 +195,16 @@ public ReactiveUpsertById upsertById(Class domainType) { @Override public String getBucketName() { - return clientFactory.getBucket().name(); + return clientFactory.getBucket().block().name(); } @Override public String getScopeName() { - return clientFactory.getScope().name(); + return clientFactory.getScope().block().name(); } @Override - public CouchbaseClientFactory getCouchbaseClientFactory() { + public ReactiveCouchbaseClientFactory getCouchbaseClientFactory() { return clientFactory; } @@ -147,7 +215,7 @@ public CouchbaseClientFactory getCouchbaseClientFactory() { * @return the collection instance. */ public Collection getCollection(final String collectionName) { - return clientFactory.getCollection(collectionName); + return clientFactory.getCollection(collectionName).block(); } @Override @@ -165,8 +233,9 @@ public ReactiveTemplateSupport support() { * * @param ex the exception to translate */ - protected RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) { - RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex); + RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) { + RuntimeException resolved = exceptionTranslator != null ? exceptionTranslator.translateExceptionIfPossible(ex) + : null; return resolved == null ? ex : resolved; } @@ -198,4 +267,190 @@ public QueryScanConsistency getConsistency() { return scanConsistency; } + protected Mono doGetDatabase() { + return ReactiveCouchbaseClientUtils.getDatabase(clientFactory, SessionSynchronization.ON_ACTUAL_TRANSACTION); + } + + protected Mono doGetTemplate() { + return ReactiveCouchbaseClientUtils.getTemplate(clientFactory, SessionSynchronization.ON_ACTUAL_TRANSACTION, + this.getConverter()); + } + + /* + private Flux withSession(ReactiveSessionCallback action, ClientSession session) { + + ReactiveSessionBoundCouchbaseTemplate operations = new ReactiveSessionBoundCouchbaseTemplate(session, + ReactiveCouchbaseTemplate.this); + + return Flux.from(action.doInSession(operations)) // + .contextWrite(ctx -> ReactiveMongoContext.setSession(ctx, Mono.just(session))); + } + */ + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(com.mongodb.session.ClientSession) + */ + public ReactiveCouchbaseOperations withCore(ReactiveCouchbaseResourceHolder core) { + return new ReactiveSessionBoundCouchbaseTemplate(core, ReactiveCouchbaseTemplate.this); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#withSession(com.mongodb.ClientSessionOptions) + */ + /* + @Override + public ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions) { + return withSession(mongoDatabaseFactory.getSession(sessionOptions)); + } + + */ + + /** + * {@link CouchbaseTemplate} extension bound to a specific {@link CoreTransactionAttemptContext} that is applied when + * interacting with the server through the driver API.
+ * The prepare steps for {} and {} proxy the target and invoke the desired target method matching the actual arguments + * plus a {@link CoreTransactionAttemptContext}. + * + * @author Christoph Strobl + * @since 2.1 + */ + static class ReactiveSessionBoundCouchbaseTemplate extends ReactiveCouchbaseTemplate { + + private final ReactiveCouchbaseTemplate delegate; + private final ReactiveCouchbaseResourceHolder holder; + + /** + * @param holder must not be {@literal null}. + * @param that must not be {@literal null}. + */ + ReactiveSessionBoundCouchbaseTemplate(ReactiveCouchbaseResourceHolder holder, ReactiveCouchbaseTemplate that) { + + super(that.clientFactory.withCore(holder), that.getConverter()); + + this.delegate = that; + this.holder = holder; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoTemplate#getCollection(java.lang.String) + */ + @Override + public Collection getCollection(String collectionName) { + + // native MongoDB objects that offer methods with ClientSession must not be proxied. + return delegate.getCollection(collectionName); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ReactiveMongoTemplate#getMongoDatabase() + */ + @Override + public ReactiveCouchbaseClientFactory getCouchbaseClientFactory() { + + // native MongoDB objects that offer methods with ClientSession must not be proxied. + return delegate.getCouchbaseClientFactory(); + } + } + + class IndexCreatorEventListener implements ApplicationListener> { + + final Consumer subscriptionExceptionHandler; + + public IndexCreatorEventListener(Consumer subscriptionExceptionHandler) { + this.subscriptionExceptionHandler = subscriptionExceptionHandler; + } + + @Override + public void onApplicationEvent(MappingContextEvent event) { + + if (!event.wasEmittedBy(converter.getMappingContext())) { + return; + } + + // PersistentEntity entity = event.getPersistentEntity(); + + // Double check type as Spring infrastructure does not consider nested generics + // if (entity instanceof MongoPersistentEntity) { + // onCheckForIndexes((MongoPersistentEntity) entity, subscriptionExceptionHandler); + // } + } + } + + /** + * Get the TransactionalOperator from
+ * 1. The template.clientFactory
+ * 2. The template.threadLocal
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() + */ + /* + private CouchbaseStuffHandle getTransactionalOperator() { + if (this.getCouchbaseClientFactory().getTransactionalOperator() != null) { + return this.getCouchbaseClientFactory().getTransactionalOperator(); + } + ReactiveCouchbaseTemplate t = this; + PseudoArgs pArgs = t.getPseudoArgs(); + if (pArgs != null && pArgs.getTxOp() != null) { + return pArgs.getTxOp(); + } + return null; + } + */ + /** + * Value object chaining together a given source document with its mapped representation and the collection to persist + * it to. + * + * @param + * @author Christoph Strobl + * @since 2.2 + */ + /* + private static class PersistableEntityModel { + + private final T source; + private final @Nullable + Document target; + private final String collection; + + private PersistableEntityModel(T source, @Nullable Document target, String collection) { + + this.source = source; + this.target = target; + this.collection = collection; + } + + static PersistableEntityModel of(T source, String collection) { + return new PersistableEntityModel<>(source, null, collection); + } + + static PersistableEntityModel of(T source, Document target, String collection) { + return new PersistableEntityModel<>(source, target, collection); + } + + PersistableEntityModel mutate(T source) { + return new PersistableEntityModel(source, target, collection); + } + + PersistableEntityModel addTargetDocument(Document target) { + return new PersistableEntityModel(source, target, collection); + } + + T getSource() { + return source; + } + + @Nullable + Document getTarget() { + return target; + } + + String getCollection() { + return collection; + } + + */ } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 5cf321b45..7d73839c6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -16,36 +16,23 @@ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; import reactor.core.publisher.Mono; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; -import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. @@ -53,23 +40,16 @@ * @author Carlos Espinaco * @since 4.2 */ -class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, ReactiveTemplateSupport { - - private static final Logger LOG = LoggerFactory.getLogger(ReactiveCouchbaseTemplateSupport.class); +class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport + implements ApplicationContextAware, ReactiveTemplateSupport { private final ReactiveCouchbaseTemplate template; - private final CouchbaseConverter converter; - private final MappingContext, CouchbasePersistentProperty> mappingContext; - private final TranslationService translationService; private ReactiveEntityCallbacks reactiveEntityCallbacks; - private ApplicationContext applicationContext; public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template, final CouchbaseConverter converter, - final TranslationService translationService) { + final TranslationService translationService) { + super(template, converter, translationService); this.template = template; - this.converter = converter; - this.mappingContext = converter.getMappingContext(); - this.translationService = translationService; } @Override @@ -84,109 +64,38 @@ public Mono encodeEntity(final Object entityToEncode) { } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, - String collection) { - return Mono.fromSupplier(() -> { - final CouchbaseDocument converted = new CouchbaseDocument(id); - converted.setId(id); - - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { // method could return a Long, Boolean, String etc. - // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left - // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a - // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. - // if this is a Collection or array, only the first element will be returned. - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - - if (cas != 0 && persistentEntity.getVersionProperty() != null) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); - } - - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); - final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - - if (persistentEntity.getVersionProperty() != null) { - accessor.setProperty(persistentEntity.getVersionProperty(), cas); - } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id, scope, collection); - return accessor.getBean(); - }); - } - - CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { - if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) { - return null; - } - return mappingContext.getPersistentEntity(entityClass); + ReactiveCouchbaseTemplate getReactiveTemplate() { + return template; } @Override - public Mono applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { - return Mono.fromSupplier(() -> { - Object returnValue; - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - if (versionProperty != null) { - accessor.setProperty(versionProperty, cas); - returnValue = accessor.getBean(); - } else { - returnValue = entity; - } - maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); - return returnValue; - }); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder) { + return decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, null); } @Override - public Mono applyUpdatedId(final Object entity, Object id) { - return Mono.fromSupplier(() -> { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); - - if (idProperty != null) { - accessor.setProperty(idProperty, id); - return accessor.getBean(); - } - return entity; - }); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); } + @Override - public Long getCas(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); - - long cas = 0; - if (versionProperty != null) { - Object casObject = accessor.getProperty(versionProperty); - if (casObject instanceof Number) { - cas = ((Number) casObject).longValue(); - } - } - return cas; + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder) { + return applyResult(entity, converted, id, cas, txResultHolder, null); } @Override - public String getJavaNameForEntity(final Class clazz) { - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); - MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); - return info.getJavaType().getName(); + public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, holder)); } - private ConvertingPropertyAccessor getPropertyAccessor(final T source) { - CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); + @Override + public Integer getTxResultHolder(T source) { + return null; } @Override @@ -212,24 +121,6 @@ public void setReactiveEntityCallbacks(ReactiveEntityCallbacks reactiveEntityCal this.reactiveEntityCallbacks = reactiveEntityCallbacks; } - public void maybeEmitEvent(CouchbaseMappingEvent event) { - if (canPublishEvent()) { - try { - this.applicationContext.publishEvent(event); - } catch (Exception e) { - LOG.warn("{} thrown during {}", e, event); - throw e; - } - } else { - LOG.info("maybeEmitEvent called, but ReactiveCouchbaseTemplate not initialized with applicationContext"); - } - - } - - private boolean canPublishEvent() { - return this.applicationContext != null; - } - protected Mono maybeCallBeforeConvert(T object, String collection) { if (reactiveEntityCallbacks != null) { return reactiveEntityCallbacks.callback(ReactiveBeforeConvertCallback.class, object, collection); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index 56b642820..778299930 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -71,11 +71,13 @@ static class ReactiveExistsByIdSupport implements ReactiveExistsById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, null, domainType); LOG.trace("existsById {}", pArgs); - return Mono.just(id) + + return TransactionalSupport.verifyNotInTransaction(template.doGetTemplate(), "existsById") + .then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) + .getBlockingCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) .map(ExistsResult::exists)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { @@ -102,7 +104,7 @@ public ExistsByIdWithOptions inCollection(final String collection) { } @Override - public TerminatingExistsById withOptions(final ExistsOptions options) { + public ExistsByIdInScope withOptions(final ExistsOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveExistsByIdSupport(template, domainType, scope, collection, options); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java index 1d661b302..2d9f1251b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java @@ -88,24 +88,12 @@ interface TerminatingFindByAnalytics extends OneAndAllReactive { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { - - /** - * Set the filter for the analytics query to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. - */ - TerminatingFindByAnalytics matching(AnalyticsQuery query); - - } - /** * Fluent method to specify options. * * @param the entity type to use. */ - interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, WithAnalyticsOptions { + interface FindByAnalyticsWithOptions extends TerminatingFindByAnalytics, WithAnalyticsOptions { /** * Fluent method to specify options to use for execution * @@ -115,65 +103,47 @@ interface FindByAnalyticsWithOptions extends FindByAnalyticsWithQuery, Wit TerminatingFindByAnalytics withOptions(AnalyticsOptions options); } - /** - * Fluent method to specify the collection. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInCollection extends FindByAnalyticsWithOptions, InCollection { - /** - * With a different collection - * - * @param collection the collection to use. - */ - @Override - FindByAnalyticsWithOptions inCollection(String collection); - } + @Deprecated + interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithOptions { - /** - * Fluent method to specify the scope. - * - * @param the entity type to use for the results. - */ - interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { /** - * With a different scope + * Allows to override the default scan consistency. * - * @param scope the scope to use. + * @param scanConsistency the custom scan consistency to use for this analytics query. */ - @Override - FindByAnalyticsInCollection inScope(String scope); + @Deprecated + FindByAnalyticsWithOptions consistentWith(AnalyticsScanConsistency scanConsistency); + } - @Deprecated - interface FindByAnalyticsConsistentWith extends FindByAnalyticsInScope { + interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWith, WithAnalyticsConsistency { /** * Allows to override the default scan consistency. * * @param scanConsistency the custom scan consistency to use for this analytics query. */ - @Deprecated - FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); + @Override + FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); } - interface FindByAnalyticsWithConsistency extends FindByAnalyticsInScope, WithAnalyticsConsistency { + interface FindByAnalyticsWithQuery extends FindByAnalyticsWithConsistency, WithAnalyticsQuery { /** - * Allows to override the default scan consistency. + * Set the filter for the analytics query to be used. * - * @param scanConsistency the custom scan consistency to use for this analytics query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ - @Override - FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scanConsistency); + FindByAnalyticsWithConsistency matching(AnalyticsQuery query); } /** * Result type override (Optional). */ - interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistency { + interface FindByAnalyticsWithProjection extends FindByAnalyticsWithQuery { /** * Define the target type fields should be mapped to.
@@ -183,9 +153,39 @@ interface FindByAnalyticsWithProjection extends FindByAnalyticsWithConsistenc * @return new instance of {@link FindByAnalyticsWithConsistency}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByAnalyticsWithConsistency as(Class returnType); + FindByAnalyticsWithQuery as(Class returnType); + } + + /** + * Fluent method to specify the collection. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInCollection extends FindByAnalyticsWithProjection, InCollection { + /** + * With a different collection + * + * @param collection the collection to use. + */ + @Override + FindByAnalyticsWithProjection inCollection(String collection); + } + + /** + * Fluent method to specify the scope. + * + * @param the entity type to use for the results. + */ + interface FindByAnalyticsInScope extends FindByAnalyticsInCollection, InScope { + /** + * With a different scope + * + * @param scope the scope to use. + */ + @Override + FindByAnalyticsInCollection inScope(String scope); } - interface ReactiveFindByAnalytics extends FindByAnalyticsWithProjection, FindByAnalyticsConsistentWith {} + interface ReactiveFindByAnalytics extends FindByAnalyticsInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index b46a81f53..ea47bc77e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -70,7 +70,7 @@ static class ReactiveFindByAnalyticsSupport implements ReactiveFindByAnalytic } @Override - public TerminatingFindByAnalytics matching(AnalyticsQuery query) { + public FindByAnalyticsWithConsistency matching(AnalyticsQuery query) { return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); } @@ -89,7 +89,7 @@ public FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scan } @Override - public FindByAnalyticsWithConsistency as(final Class returnType) { + public FindByAnalyticsWithQuery as(final Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); @@ -109,8 +109,9 @@ public Mono first() { public Flux all() { return Flux.defer(() -> { String statement = assembleEntityQuery(false); - return template.getCouchbaseClientFactory().getCluster().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 { @@ -133,7 +134,7 @@ public Flux all() { cas = row.getLong(TemplateUtils.SELECT_CAS); row.removeKey(TemplateUtils.SELECT_ID); row.removeKey(TemplateUtils.SELECT_CAS); - return support.decodeEntity(id, row.toString(), cas, returnType, null, null); + return support.decodeEntity(id, row.toString(), cas, returnType, null, null, null); }); }); } @@ -142,7 +143,7 @@ public Flux all() { public Mono count() { return Mono.defer(() -> { String statement = assembleEntityQuery(true); - return template.getCouchbaseClientFactory().getCluster().reactive() + return template.getCouchbaseClientFactory().getBlockingCluster().reactive() .analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -173,7 +174,7 @@ public FindByAnalyticsInCollection inScope(final String scope) { } @Override - public FindByAnalyticsWithConsistency inCollection(final String collection) { + public FindByAnalyticsWithProjection inCollection(final String collection) { return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index 5e9983b0f..4e54be960 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,11 +28,12 @@ import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithGetOptions; import org.springframework.data.couchbase.core.support.WithProjectionId; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.kv.GetOptions; /** - * Get Operations + * Get Operations - method/interface chaining is from the bottom up. * * @author Christoph Strobl * @since 2.0 @@ -67,7 +69,21 @@ interface TerminatingFindById extends OneAndAllIdReactive { * @return the list of found entities. */ Flux all(Collection ids); + } + /** + * Provide transaction + * + * @param the entity type to use for the results + */ + interface FindByIdWithTransaction extends TerminatingFindById, WithTransaction { + /** + * Provide transaction + * + * @param txCtx + * @return + */ + TerminatingFindById transaction(CouchbaseTransactionalOperator txCtx); } /** @@ -85,19 +101,45 @@ interface FindByIdWithOptions extends TerminatingFindById, WithGetOptions< TerminatingFindById withOptions(GetOptions options); } + interface FindByIdWithProjection extends FindByIdWithOptions, WithProjectionId { + /** + * Load only certain fields for the document. + * + * @param fields the projected fields to load. + */ + FindByIdWithOptions project(String... fields); + } + + interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { + /** + * Load only certain fields for the document. + * + * @param expiry the projected fields to load. + */ + @Override + FindByIdWithProjection withExpiry(Duration expiry); + } + + /** + * Interface to that can produce either transactional or non-transactional operations. + * + * @param the entity type to use for the results. + */ + interface FindByIdTxOrNot extends FindByIdWithTransaction, FindByIdWithExpiry {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface FindByIdInCollection extends FindByIdWithOptions, InCollection { + interface FindByIdInCollection extends FindByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - FindByIdWithOptions inCollection(String collection); + FindByIdTxOrNot inCollection(String collection); } /** @@ -115,32 +157,11 @@ interface FindByIdInScope extends FindByIdInCollection, InScope { FindByIdInCollection inScope(String scope); } - interface FindByIdWithProjection extends FindByIdInScope, WithProjectionId { - - /** - * Load only certain fields for the document. - * - * @param fields the projected fields to load. - */ - FindByIdInCollection project(String... fields); - - } - - interface FindByIdWithExpiry extends FindByIdWithProjection, WithExpiry { - /** - * Load only certain fields for the document. - * - * @param expiry the projected fields to load. - */ - @Override - FindByIdWithProjection withExpiry(Duration expiry); - } - /** * Provides methods for constructing query operations in a fluent way. * - * @param the entity type to use for the results + * @param the entity type. */ - interface ReactiveFindById extends FindByIdWithExpiry {} + interface ReactiveFindById extends FindByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index 347054579..475b719cc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -16,10 +16,16 @@ package org.springframework.data.couchbase.core; import static com.couchbase.client.java.kv.GetAndTouchOptions.getAndTouchOptions; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.transactions.TransactionGetResult; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.Collection; @@ -29,6 +35,7 @@ import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.error.DocumentNotFoundException; @@ -49,7 +56,7 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati @Override public ReactiveFindById findById(Class domainType) { - return new ReactiveFindByIdSupport<>(template, domainType, null, null, null, null, null, template.support()); + return new ReactiveFindByIdSupport<>(template, domainType, null, null, null, null, null, null, template.support()); } static class ReactiveFindByIdSupport implements ReactiveFindById { @@ -60,11 +67,13 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { private final String collection; private final CommonOptions options; private final List fields; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveTemplateSupport support; private final Duration expiry; ReactiveFindByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String scope, String collection, - CommonOptions options, List fields, Duration expiry, ReactiveTemplateSupport support) { + CommonOptions options, List fields, Duration expiry, CouchbaseTransactionalOperator txCtx, + ReactiveTemplateSupport support) { this.template = template; this.domainType = domainType; this.scope = scope; @@ -72,6 +81,7 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { this.options = options; this.fields = fields; this.expiry = expiry; + this.txCtx = txCtx; this.support = support; } @@ -79,32 +89,51 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { public Mono one(final String id) { CommonOptions gOptions = initGetOptions(); - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, txCtx, domainType); LOG.trace("findById {}", pArgs); - return Mono.just(id).flatMap(docId -> { - ReactiveCollection reactive = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive(); - if (pArgs.getOptions() instanceof GetAndTouchOptions) { - return reactive.getAndTouch(docId, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()); - } else { - return reactive.get(docId, (GetOptions) pArgs.getOptions()); - } - }).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, - pArgs.getScope(), pArgs.getCollection())).onErrorResume(throwable -> { - if (throwable instanceof RuntimeException) { - if (throwable instanceof DocumentNotFoundException) { - return Mono.empty(); + ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getBlockingCollection(pArgs.getCollection()).reactive(); + + // this will get me a template with a session holding tx + Mono tmpl = template.doGetTemplate(); + + Mono reactiveEntity = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null) + .flatMap(s -> { + System.err.println("Session: "+s); + //Mono reactiveEntity = Mono.defer(() -> { + if (s == null || s.getCore() == null) { + if (pArgs.getOptions() instanceof GetAndTouchOptions) { + return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); + } else { + return rc.get(id, (GetOptions) pArgs.getOptions()) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType, + pArgs.getScope(), pArgs.getCollection(), null)); } - } - return Mono.error(throwable); - }).onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { - return throwable; + 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) { + return Mono.empty(); + } + return Mono.error(throwable); + }).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); + } @Override @@ -113,31 +142,42 @@ public Flux all(final Collection ids) { } @Override - public TerminatingFindById withOptions(final GetOptions options) { + public FindByIdInScope withOptions(final GetOptions options) { Assert.notNull(options, "Options must not be null."); - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } @Override - public FindByIdWithOptions inCollection(final String collection) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + public FindByIdInCollection inCollection(final String collection) { + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } @Override public FindByIdInCollection inScope(final String scope) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } @Override - public FindByIdInScope project(String... fields) { + public FindByIdWithOptions project(String... fields) { Assert.notNull(fields, "Fields must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), - expiry, support); + expiry, txCtx, support); } @Override public FindByIdWithProjection withExpiry(final Duration expiry) { - return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); + } + + @Override + public FindByIdWithProjection transaction(CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null"); + return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, txCtx, + support); } private CommonOptions initGetOptions() { @@ -172,6 +212,7 @@ private Duration expiryToUse() { } return expiryToUse; } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index 9a839ed7a..89df6ae9c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,6 +29,7 @@ import org.springframework.data.couchbase.core.support.WithDistinct; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -92,94 +94,99 @@ interface TerminatingFindByQuery extends OneAndAllReactive { } /** - * Fluent methods to filter by query + * Fluent method to specify options. * * @param the entity type to use for the results. */ - interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { - + interface FindByQueryWithOptions extends TerminatingFindByQuery, WithQueryOptions { /** - * Set the filter {@link Query} to be used. - * - * @param query must not be {@literal null}. - * @throws IllegalArgumentException if query is {@literal null}. + * @param options options to use for execution */ - TerminatingFindByQuery matching(Query query); + TerminatingFindByQuery withOptions(QueryOptions options); + } + + /** + * To be removed at the next major release. use WithConsistency instead + * + * @param the entity type to use for the results. + */ + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryWithOptions { /** - * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * Allows to override the default scan consistency. * - * @param criteria must not be {@literal null}. - * @return new instance of {@link TerminatingFindByQuery}. - * @throws IllegalArgumentException if criteria is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ - default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } - + @Deprecated + FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); } /** - * Fluent method to specify options. - * + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * * @param the entity type to use for the results. */ - interface FindByQueryWithOptions extends FindByQueryWithQuery, WithQueryOptions { + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + /** - * @param options options to use for execution + * Allows to override the default scan consistency. + * + * @param scanConsistency the custom scan consistency to use for this query. */ - TerminatingFindByQuery withOptions(QueryOptions options); + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + } /** - * Fluent method to specify the collection - * + * Fluent method to add transactions + * * @param the entity type to use for the results. */ - interface FindByQueryInCollection extends FindByQueryWithOptions, InCollection { - FindByQueryWithOptions inCollection(String collection); + interface FindByQueryWithTransaction extends TerminatingFindByQuery, WithTransaction { + + /** + * Finds the distinct values for a specified {@literal field} across a single {@link } or view. + * + * @param txCtx Must not be {@literal null}. + * @return new instance of {@link ReactiveFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. + */ + TerminatingFindByQuery transaction(CouchbaseTransactionalOperator txCtx); } /** - * Fluent method to specify the scope + * Fluent interface for operations with or without a transaction. * * @param the entity type to use for the results. */ - interface FindByQueryInScope extends FindByQueryInCollection, InScope { - FindByQueryInCollection inScope(String scope); - } + interface FindByQueryTxOrNot extends FindByQueryWithConsistency, FindByQueryWithTransaction {} /** - * To be removed at the next major release. use WithConsistency instead + * Fluent methods to filter by query * * @param the entity type to use for the results. */ - @Deprecated - interface FindByQueryConsistentWith extends FindByQueryInScope { + interface FindByQueryWithQuery extends FindByQueryTxOrNot, WithQuery { /** - * Allows to override the default scan consistency. + * Set the filter {@link Query} to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param query must not be {@literal null}. + * @throws IllegalArgumentException if query is {@literal null}. */ - @Deprecated - FindByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. - * - * @param the entity type to use for the results. - */ - interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { + FindByQueryTxOrNot matching(Query query); /** - * Allows to override the default scan consistency. + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param criteria must not be {@literal null}. + * @return new instance of {@link TerminatingFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. */ - FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + default FindByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } @@ -188,7 +195,7 @@ interface FindByQueryWithConsistency extends FindByQueryConsistentWith, Wi * * @param the entity type to use for the results. */ - interface FindByQueryWithProjection extends FindByQueryWithConsistency { + interface FindByQueryWithProjection extends FindByQueryWithQuery { /** * Define the target type fields should be mapped to.
@@ -198,7 +205,7 @@ interface FindByQueryWithProjection extends FindByQueryWithConsistency { * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if returnType is {@literal null}. */ - FindByQueryWithConsistency as(Class returnType); + FindByQueryWithQuery as(Class returnType); } /** @@ -233,7 +240,25 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * @return new instance of {@link ReactiveFindByQuery}. * @throws IllegalArgumentException if field is {@literal null}. */ - FindByQueryWithProjection distinct(String[] distinctFields); + FindByQueryWithProjecting distinct(String[] distinctFields); + } + + /** + * Fluent method to specify the collection + * + * @param the entity type to use for the results. + */ + interface FindByQueryInCollection extends FindByQueryWithDistinct, InCollection { + FindByQueryWithDistinct inCollection(String collection); + } + + /** + * Fluent method to specify the scope + * + * @param the entity type to use for the results. + */ + interface FindByQueryInScope extends FindByQueryInCollection, InScope { + FindByQueryInCollection inScope(String scope); } /** @@ -241,6 +266,6 @@ interface FindByQueryWithDistinct extends FindByQueryWithProjecting, WithD * * @param the entity type to use for the results */ - interface ReactiveFindByQuery extends FindByQueryWithDistinct {} + interface ReactiveFindByQuery extends FindByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index cfa3e0f34..8e0ccf310 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,20 +15,27 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +import com.couchbase.client.java.transactions.TransactionQueryResult; /** * {@link ReactiveFindByQueryOperation} implementations for Couchbase. @@ -50,7 +57,7 @@ public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate templ @Override public ReactiveFindByQuery findByQuery(final Class domainType) { return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, null, null, null, null, null, - null, template.support()); + null, null, template.support()); } static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @@ -65,12 +72,13 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final String[] distinctFields; private final String[] fields; private final QueryOptions options; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveTemplateSupport support; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, - final Class returnType, final Query query, final QueryScanConsistency scanConsistency, final String scope, - final String collection, final QueryOptions options, final String[] distinctFields, final String[] fields, - 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; @@ -84,57 +92,58 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { this.distinctFields = distinctFields; this.fields = fields; this.support = support; + this.txCtx = txCtx; } @Override 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; } return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanCons, scope, collection, - options, distinctFields, fields, support); + options, distinctFields, fields, txCtx, support); } @Override - public TerminatingFindByQuery withOptions(final QueryOptions options) { + public FindByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override public FindByQueryInCollection inScope(final String scope) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override - public FindByQueryWithConsistency inCollection(final String collection) { + public FindByQueryWithDistinct inCollection(final String collection) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override @Deprecated - public FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency) { + public FindByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override public FindByQueryWithConsistency withConsistency(QueryScanConsistency scanConsistency) { return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } - public FindByQueryWithConsistency as(Class returnType) { + public FindByQueryWithProjecting as(Class returnType) { Assert.notNull(returnType, "returnType must not be null!"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override @@ -142,7 +151,7 @@ public FindByQueryWithProjection project(String[] fields) { Assert.notNull(fields, "Fields must not be null"); Assert.isNull(distinctFields, "only one of project(fields) and distinct(distinctFields) can be specified"); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, fields, support); + collection, options, distinctFields, fields, txCtx, support); } @Override @@ -154,7 +163,14 @@ public FindByQueryWithDistinct distinct(final String[] distinctFields) { // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, dFields, fields, support); + collection, options, dFields, fields, txCtx, support); + } + + @Override + public FindByQueryWithTransaction transaction(CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null!"); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, + collection, options, distinctFields, fields, txCtx, support); } @Override @@ -169,41 +185,55 @@ public Mono first() { @Override public Flux all() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - Mono allResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return Flux.defer(() -> allResult.onErrorMap(throwable -> { + + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.getBlockingScope(pArgs.getScope()).reactive(); + Mono tmpl = template.doGetTemplate(); + + Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + if (s.getCore() == null) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getCluster().block().reactive().query(statement, opts) + : rs.query(statement, opts); + } else { + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getCluster().block().environment().jsonSerializer())).query(statement, opts); + } + })); + + return allResult.onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); } else { return throwable; } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> { + }).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)); + 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)); + 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()); - })); + return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(), + null); + }); } public QueryOptions buildOptions(QueryOptions options) { @@ -211,24 +241,42 @@ public QueryOptions buildOptions(QueryOptions options) { return query.buildQueryOptions(options, qsc); } + private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { + TransactionQueryOptions opts = OptionsBuilder.buildTransactionQueryOptions(buildOptions(options)); + return opts; + } + @Override public Mono count() { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); LOG.trace("findByQuery {} statement: {}", pArgs, statement); - Mono countResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return Mono.defer(() -> countResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.getBlockingScope(pArgs.getScope()).reactive(); + Mono tmpl = template.doGetTemplate(); + + Mono allResult = tmpl.flatMap(tp -> tp.getCouchbaseClientFactory().getTransactionResources(null).flatMap(s -> { + if (s.getCore() == null) { + QueryOptions opts = buildOptions(pArgs.getOptions()); + return pArgs.getScope() == null ? clientFactory.getBlockingCluster().reactive().query(statement, opts) + : rs.query(statement, opts); } else { - return throwable; + TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions()); + return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.getCore(), + clientFactory.getBlockingCluster().environment().jsonSerializer())).query(statement, opts); } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong(row.getNames().iterator().next())) - .next()); + })); + + 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())) + .map(row -> row.getLong(row.getNames().iterator().next())).elementAt(0); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 0e1372b4f..cadca3839 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -72,13 +72,14 @@ public Mono any(final String id) { if (garOptions.build().transcoder() == null) { garOptions.transcoder(RawJsonTranscoder.INSTANCE); } - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, null, + domainType); LOG.trace("getAnyReplica {}", pArgs); - return Mono.just(id) + return TransactionalSupport.verifyNotInTransaction(template.doGetTemplate(), "findFromReplicasById") + .then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, - pArgs.getScope(), pArgs.getCollection())) + .getBlockingCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType, pArgs.getScope(), pArgs.getCollection(), null)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 6439879fe..a2818d21a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +28,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithInsertOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.InsertOptions; @@ -84,17 +86,43 @@ interface InsertByIdWithOptions extends TerminatingInsertById, WithInsertO TerminatingInsertById withOptions(InsertOptions options); } + interface InsertByIdWithDurability extends InsertByIdWithOptions, WithDurability { + + @Override + InsertByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + @Override + InsertByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { + + @Override + InsertByIdWithDurability withExpiry(Duration expiry); + } + + interface InsertByIdWithTransaction extends TerminatingInsertById, WithTransaction { + @Override + InsertByIdWithDurability transaction(CouchbaseTransactionalOperator txCtx); + } + /** * Fluent method to specify the collection. */ - interface InsertByIdInCollection extends InsertByIdWithOptions, InCollection { + interface InsertByIdTxOrNot extends InsertByIdWithTransaction, InsertByIdWithExpiry {} + + /** + * Fluent method to specify the collection. + */ + interface InsertByIdInCollection extends InsertByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - InsertByIdWithOptions inCollection(String collection); + InsertByIdTxOrNot inCollection(String collection); } /** @@ -110,27 +138,11 @@ interface InsertByIdInScope extends InsertByIdInCollection, InScope { InsertByIdInCollection inScope(String scope); } - interface InsertByIdWithDurability extends InsertByIdInScope, WithDurability { - - @Override - InsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - InsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { - - @Override - InsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV insert operations in a fluent way. * * @param the entity type to insert */ - interface ReactiveInsertById extends InsertByIdWithExpiry {} + interface ReactiveInsertById extends InsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 0d319fb4a..4de3a5f3c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -15,9 +15,11 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.Method; import java.time.Duration; import java.util.Collection; @@ -26,6 +28,9 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -33,6 +38,8 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -46,7 +53,7 @@ public ReactiveInsertByIdOperationSupport(final ReactiveCouchbaseTemplate templa public ReactiveInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveInsertByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null, template.support()); + DurabilityLevel.NONE, null, (TransactionalOperator) null, template.support()); } static class ReactiveInsertByIdSupport implements ReactiveInsertById { @@ -60,11 +67,14 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final CouchbaseTransactionalOperator txCtx; + private final TransactionalOperator txOp; private final ReactiveTemplateSupport support; ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, Duration expiry, 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; @@ -74,28 +84,78 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; + this.txOp = null; + this.support = support; + } + + ReactiveInsertByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final InsertOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + final DurabilityLevel durabilityLevel, Duration expiry, TransactionalOperator txOp, + ReactiveTemplateSupport support) { + this.template = template; + this.domainType = domainType; + this.scope = scope; + this.collection = collection; + this.options = options; + this.persistTo = persistTo; + this.replicateTo = replicateTo; + this.durabilityLevel = durabilityLevel; + this.expiry = expiry; + this.txCtx = null; + this.txOp = txOp; this.support = support; } @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, txCtx, domainType); LOG.trace("insertById {}", pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } + System.err.println("txOp: " + pArgs.getTxOp()); + Mono tmpl = template.doGetTemplate(); + + 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)), + (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) { + CoreTransactionGetResult internal; + try { + // Method method = CoreTransactionGetResult.class.getDeclaredMethod("internal"); + // method.setAccessible(true); + // internal = (CoreTransactionGetResult) method.invoke(getResult); + } catch (Throwable err) { + throw new RuntimeException(err); + } + return getResult.cas(); + } + @Override public Flux all(Collection objects) { return Flux.fromIterable(objects).flatMap(this::one); @@ -109,42 +169,52 @@ public InsertOptions buildOptions(InsertOptions options, CouchbaseDocument doc) public TerminatingInsertById withOptions(final InsertOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public InsertByIdInCollection inScope(final String scope) { return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override - public InsertByIdWithOptions inCollection(final String collection) { + public InsertByIdTxOrNot inCollection(final String collection) { return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public InsertByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } + // 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) { Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public InsertByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } + + @Override + public InsertByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, txCtx, support); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index a9abaa178..c827b02c7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,6 +26,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithRemoveOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -63,6 +65,13 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { @Override Mono one(String id); + /** + * Remove one document. Requires whole entity for transaction to have the cas. + * + * @param entity the entity + * @return result of the remove + */ + Mono oneEntity(Object entity); /** * Remove the documents in the collection. * @@ -71,6 +80,13 @@ interface TerminatingRemoveById extends OneAndAllIdReactive { */ @Override Flux all(Collection ids); + /** + * Remove the documents in the collection. Requires whole entity for transaction to have the cas. + * + * @param ids the document IDs. + * @return result of the removes. + */ + Flux allEntities(Collection ids); } @@ -86,22 +102,42 @@ interface RemoveByIdWithOptions extends TerminatingRemoveById, WithRemoveOptions TerminatingRemoveById withOptions(RemoveOptions options); } + interface RemoveByIdWithDurability extends RemoveByIdWithOptions, WithDurability { + @Override + RemoveByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + @Override + RemoveByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface RemoveByIdWithCas extends RemoveByIdWithDurability { + + RemoveByIdWithDurability withCas(Long cas); + } + + interface RemoveByIdWithTransaction extends RemoveByIdWithCas, WithTransaction { + RemoveByIdWithCas transaction(CouchbaseTransactionalOperator txCtx); + } + + interface RemoveByIdTxOrNot extends RemoveByIdWithCas, RemoveByIdWithTransaction {} + /** * Fluent method to specify the collection. */ - interface RemoveByIdInCollection extends RemoveByIdWithOptions, InCollection { + interface RemoveByIdInCollection extends RemoveByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByIdWithOptions inCollection(String collection); + RemoveByIdTxOrNot inCollection(String collection); } /** * Fluent method to specify the scope. */ - interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { + interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { /** * With a different scope * @@ -110,23 +146,9 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { RemoveByIdInCollection inScope(String scope); } - interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { - @Override - RemoveByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - RemoveByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface RemoveByIdWithCas extends RemoveByIdWithDurability { - - RemoveByIdWithDurability withCas(Long cas); - } - /** * Provides methods for constructing remove operations in a fluent way. */ - interface ReactiveRemoveById extends RemoveByIdWithCas {} + interface ReactiveRemoveById extends RemoveByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 547c561e5..fb8f3ee84 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,9 +15,17 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.error.CasMismatchException; +import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.transactions.TransactionGetResult; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.lang.reflect.Method; import java.util.Collection; import org.slf4j.Logger; @@ -27,10 +35,13 @@ import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.ReactiveCollection; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.RemoveOptions; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -49,7 +60,7 @@ public ReactiveRemoveById removeById() { @Override public ReactiveRemoveById removeById(Class domainType) { return new ReactiveRemoveByIdSupport(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null); + DurabilityLevel.NONE, null, null); } static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { @@ -63,10 +74,11 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Long cas; + 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) { + 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; @@ -76,23 +88,65 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.cas = cas; + this.txCtx = txCtx; } @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("removeById {}", pArgs); - return Mono.just(id) - .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())) - .map(r -> RemoveResult.from(docId, r))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).block() + .reactive(); + 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"); + if ( cas == null || cas == 0 ){ + throw new IllegalArgumentException("cas must be supplied for tx remove"); + } + Mono gr = s.getCore().get(makeCollectionIdentifier(rc.async()), id); + + return gr.flatMap(getResult -> { + 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; + } + })); + 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, + durabilityLevel, template.support().getCas(entity), txCtx); + return op.one(template.support().getId(entity).toString()); } @Override @@ -100,6 +154,11 @@ public Flux all(final Collection ids) { return Flux.fromIterable(ids).flatMap(this::one); } + @Override + public Flux allEntities(Collection entities) { + return Flux.fromIterable(entities).flatMap(this::oneEntity); + } + private RemoveOptions buildRemoveOptions(RemoveOptions options) { return OptionsBuilder.buildRemoveOptions(options, persistTo, replicateTo, durabilityLevel, cas); } @@ -108,7 +167,7 @@ private RemoveOptions buildRemoveOptions(RemoveOptions options) { public RemoveByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override @@ -116,33 +175,40 @@ public RemoveByIdInScope withDurability(final PersistTo persistTo, final Replica Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override - public RemoveByIdWithDurability inCollection(final String collection) { + public RemoveByIdTxOrNot inCollection(final String collection) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdInCollection inScope(final String scope) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public TerminatingRemoveById withOptions(final RemoveOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } @Override public RemoveByIdWithDurability withCas(Long cas) { return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, cas); + durabilityLevel, cas, txCtx); } + + @Override + public RemoveByIdWithCas transaction(CouchbaseTransactionalOperator txCtx) { + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, cas, txCtx); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 7619eabf1..95aed499c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import org.springframework.data.couchbase.core.query.Query; @@ -24,6 +25,7 @@ import org.springframework.data.couchbase.core.support.WithConsistency; import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.data.couchbase.core.support.WithQueryOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -54,31 +56,62 @@ interface TerminatingRemoveByQuery { } /** - * Fluent methods to specify the query + * Fluent method to specify options. * - * @param the entity type. + * @param the entity type to use for the results. */ - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { + interface RemoveByQueryWithOptions extends TerminatingRemoveByQuery, WithQueryOptions { + /** + * Fluent method to specify options to use for execution + * + * @param options to use for execution + */ + TerminatingRemoveByQuery withOptions(QueryOptions options); + } - TerminatingRemoveByQuery matching(Query query); + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryWithOptions { + + @Deprecated + RemoveByQueryWithOptions consistentWith(QueryScanConsistency scanConsistency); + + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { + @Override + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { - return matching(Query.query(criteria)); - } } /** - * Fluent method to specify options. + * Fluent method to specify the transaction * * @param the entity type to use for the results. */ - interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQueryOptions { + interface RemoveByQueryWithTransaction extends TerminatingRemoveByQuery, WithTransaction { /** - * Fluent method to specify options to use for execution + * Provide the transaction * - * @param options to use for execution + * @param txCtx - transaction */ - RemoveByQueryWithQuery withOptions(QueryOptions options); + @Override + TerminatingRemoveByQuery transaction(CouchbaseTransactionalOperator txCtx); + } + + interface RemoveByQueryTxOrNot extends RemoveByQueryWithConsistency, RemoveByQueryWithTransaction {} + + /** + * Fluent methods to specify the query + * + * @param the entity type. + */ + interface RemoveByQueryWithQuery extends RemoveByQueryTxOrNot, WithQuery { + + RemoveByQueryTxOrNot matching(Query query); + + default RemoveByQueryTxOrNot matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } /** @@ -86,13 +119,13 @@ interface RemoveByQueryWithOptions extends RemoveByQueryWithQuery, WithQue * * @param the entity type to use for the results. */ - interface RemoveByQueryInCollection extends RemoveByQueryWithOptions, InCollection { + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, InCollection { /** * With a different collection * * @param collection the collection to use. */ - RemoveByQueryWithOptions inCollection(String collection); + RemoveByQueryWithQuery inCollection(String collection); } /** @@ -109,25 +142,11 @@ interface RemoveByQueryInScope extends RemoveByQueryInCollection, InScope< RemoveByQueryInCollection inScope(String scope); } - @Deprecated - interface RemoveByQueryConsistentWith extends RemoveByQueryInScope { - - @Deprecated - RemoveByQueryInScope consistentWith(QueryScanConsistency scanConsistency); - - } - - interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - @Override - RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); - - } - /** * Provides methods for constructing query operations in a fluent way. * * @param the entity type. */ - interface ReactiveRemoveByQuery extends RemoveByQueryWithConsistency {} + interface ReactiveRemoveByQuery extends RemoveByQueryInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index d6ea991d6..d21bcab01 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,6 +15,10 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.java.transactions.TransactionQueryOptions; +import com.couchbase.client.java.transactions.TransactionQueryResult; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +31,7 @@ import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.util.Assert; +import com.couchbase.client.java.ReactiveScope; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; @@ -44,7 +49,7 @@ public ReactiveRemoveByQueryOperationSupport(final ReactiveCouchbaseTemplate tem @Override public ReactiveRemoveByQuery removeByQuery(Class domainType) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null); + return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null, null); } static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery { @@ -56,9 +61,11 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final String scope; private final String collection; private final QueryOptions options; + private final CouchbaseTransactionalOperator txCtx; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options) { + final QueryScanConsistency scanConsistency, String scope, String collection, QueryOptions options, + CouchbaseTransactionalOperator txCtx) { this.template = template; this.domainType = domainType; this.query = query; @@ -66,25 +73,34 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery this.scope = scope; this.collection = collection; this.options = options; + this.txCtx = txCtx; } @Override public Flux all() { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); String statement = assembleDeleteQuery(pArgs.getCollection()); LOG.trace("removeByQuery {} statement: {}", pArgs, statement); - Mono allResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())); - return Flux.defer(() -> allResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject) + Mono allResult = null; + ReactiveCouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory(); + ReactiveScope rs = clientFactory.getBlockingScope(pArgs.getScope()).reactive(); + if (pArgs.getTxOp() == null) { + QueryOptions opts = buildQueryOptions(pArgs.getOptions()); + allResult = pArgs.getScope() == null ? clientFactory.getBlockingCluster().reactive().query(statement, opts) + : rs.query(statement, opts); + } 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 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) .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), Optional.empty()))); } @@ -94,29 +110,34 @@ private QueryOptions buildQueryOptions(QueryOptions options) { return query.buildQueryOptions(options, qsc); } + private TransactionQueryOptions buildTransactionOptions(QueryOptions options) { + TransactionQueryOptions txOptions = TransactionQueryOptions.queryOptions(); + return txOptions; + } + @Override - public TerminatingRemoveByQuery matching(final Query query) { + public RemoveByQueryTxOrNot matching(final Query query) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override - public RemoveByQueryWithConsistency inCollection(final String collection) { + public RemoveByQueryWithQuery inCollection(final String collection) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override @Deprecated public RemoveByQueryInScope consistentWith(final QueryScanConsistency scanConsistency) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } private String assembleDeleteQuery(String collection) { @@ -127,14 +148,21 @@ private String assembleDeleteQuery(String collection) { public RemoveByQueryWithQuery withOptions(final QueryOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } @Override public RemoveByQueryInCollection inScope(final String scope) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, - options); + options, txCtx); } + + @Override + public RemoveByQueryWithConsistency transaction(final CouchbaseTransactionalOperator txCtx) { + return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, + options, txCtx); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index 0341096cb..deda7dd9b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +28,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.core.support.WithReplaceOptions; +import org.springframework.data.couchbase.core.support.WithTransaction; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -84,19 +86,39 @@ interface ReplaceByIdWithOptions extends TerminatingReplaceById, WithRepla TerminatingReplaceById withOptions(ReplaceOptions options); } + interface ReplaceByIdWithDurability extends ReplaceByIdWithOptions, WithDurability { + + ReplaceByIdInCollection withDurability(DurabilityLevel durabilityLevel); + + ReplaceByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); + + } + + interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { + + ReplaceByIdWithDurability withExpiry(final Duration expiry); + } + + interface ReplaceByIdWithTransaction extends TerminatingReplaceById, WithTransaction { + @Override + ReplaceByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx); + } + + interface ReplaceByIdTxOrNot extends ReplaceByIdWithExpiry, ReplaceByIdWithTransaction {} + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface ReplaceByIdInCollection extends ReplaceByIdWithOptions, InCollection { + interface ReplaceByIdInCollection extends ReplaceByIdTxOrNot, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - ReplaceByIdWithOptions inCollection(String collection); + ReplaceByIdTxOrNot inCollection(String collection); } /** @@ -114,24 +136,11 @@ interface ReplaceByIdInScope extends ReplaceByIdInCollection, InScope inScope(String scope); } - interface ReplaceByIdWithDurability extends ReplaceByIdInScope, WithDurability { - - ReplaceByIdInScope withDurability(DurabilityLevel durabilityLevel); - - ReplaceByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface ReplaceByIdWithExpiry extends ReplaceByIdWithDurability, WithExpiry { - - ReplaceByIdWithDurability withExpiry(final Duration expiry); - } - /** * Provides methods for constructing KV replace operations in a fluent way. * * @param the entity type to replace */ - interface ReactiveReplaceById extends ReplaceByIdWithExpiry {} + interface ReactiveReplaceById extends ReplaceByIdInScope {}; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index ba96de24c..383ed9c5f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -15,9 +15,19 @@ */ package org.springframework.data.couchbase.core; +import com.couchbase.client.core.error.CasMismatchException; +import com.couchbase.client.core.error.transaction.RetryTransactionException; +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.transactions.TransactionGetResult; +import 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; +import java.lang.reflect.Method; import java.time.Duration; import java.util.Collection; @@ -26,13 +36,17 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; import org.springframework.util.Assert; +import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.ReplicateTo; +import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier; + public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { private final ReactiveCouchbaseTemplate template; @@ -46,7 +60,7 @@ public ReactiveReplaceByIdOperationSupport(final ReactiveCouchbaseTemplate templ public ReactiveReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveReplaceByIdSupport<>(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, - DurabilityLevel.NONE, null, template.support()); + DurabilityLevel.NONE, null, null, template.support()); } static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { @@ -60,11 +74,13 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; + private final CouchbaseTransactionalOperator txCtx; private final ReactiveTemplateSupport support; ReactiveReplaceByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, - final String collection, final ReplaceOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, - final DurabilityLevel durabilityLevel, final Duration expiry, 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; @@ -74,26 +90,57 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; + this.txCtx = txCtx; this.support = support; } @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, txCtx, domainType); LOG.trace("replaceById {}", pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .replace(converted.getId(), converted.export(), - buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyUpdatedCas(object, converted, result.cas()))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; + Mono tmpl = template.doGetTemplate(); + + 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)); + }, (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); } + + 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) { + 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()); + }).flatMap(result -> this.support.applyResult(object, converted, converted.getId(), 0L, null, 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"); + } } @Override @@ -110,26 +157,26 @@ private ReplaceOptions buildReplaceOptions(ReplaceOptions options, T object, Cou public TerminatingReplaceById withOptions(final ReplaceOptions options) { Assert.notNull(options, "Options must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override - public ReplaceByIdWithDurability inCollection(final String collection) { + public ReplaceByIdTxOrNot inCollection(final String collection) { return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public ReplaceByIdInCollection inScope(final String scope) { return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public ReplaceByIdInScope withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override @@ -137,14 +184,21 @@ public ReplaceByIdInScope withDurability(final PersistTo persistTo, final Rep Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); } @Override public ReplaceByIdWithDurability withExpiry(final Duration expiry) { Assert.notNull(expiry, "expiry must not be null."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, - durabilityLevel, expiry, support); + durabilityLevel, expiry, txCtx, support); + } + + @Override + public ReplaceByIdWithExpiry transaction(final CouchbaseTransactionalOperator txCtx) { + Assert.notNull(txCtx, "txCtx must not be null."); + return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, + durabilityLevel, expiry, txCtx, support); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveSessionCallback.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveSessionCallback.java new file mode 100644 index 000000000..c91caa162 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveSessionCallback.java @@ -0,0 +1,45 @@ +package org.springframework.data.couchbase.core; +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.reactivestreams.Publisher; + +/** + * Callback interface for executing operations within a {@link com.mongodb.reactivestreams.client.ClientSession} using + * reactive infrastructure. + * + * @author Christoph Strobl + * @since 2.1 + * @see com.mongodb.reactivestreams.client.ClientSession + */ +@FunctionalInterface +public interface ReactiveSessionCallback { + + /** + * Execute operations against a MongoDB instance via session bound {@link ReactiveMongoOperations}. The session is + * inferred directly into the operation so that no further interaction is necessary.
+ * Please note that only Spring Data-specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and + * others are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway + * objects like {@link com.mongodb.reactivestreams.client.MongoCollection} or + * {@link com.mongodb.reactivestreams.client.MongoDatabase} via eg. + * {@link ReactiveMongoOperations#getCollection(String)} we leave responsibility for + * {@link com.mongodb.session.ClientSession} again up to the caller. + * + * @param operations will never be {@literal null}. + * @return never {@literal null}. + */ + Publisher doInSession(ReactiveCouchbaseOperations operations); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 0fa725574..1313a646d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -17,26 +17,39 @@ import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; /** - * * @author Michael Reiche */ public interface ReactiveTemplateSupport { Mono encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection); + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder); + + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); - Mono applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); + Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder); - Mono applyUpdatedId(T entity, Object id); + Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, + TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); Long getCas(Object entity); + Object getId(Object entity); + String getJavaNameForEntity(Class clazz); - void maybeEmitEvent(CouchbaseMappingEvent event); + Integer getTxResultHolder(T source); + + // Integer setTxResultHolder(T source); + + TranslationService getTranslationService(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java index 05249e198..b3045a25b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java @@ -86,19 +86,32 @@ interface UpsertByIdWithOptions extends TerminatingUpsertById, WithUpsertO TerminatingUpsertById withOptions(UpsertOptions options); } + interface UpsertByIdWithDurability extends UpsertByIdWithOptions, WithDurability { + @Override + UpsertByIdWithOptions withDurability(DurabilityLevel durabilityLevel); + + @Override + UpsertByIdWithOptions withDurability(PersistTo persistTo, ReplicateTo replicateTo); + } + + interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { + @Override + UpsertByIdWithDurability withExpiry(Duration expiry); + } + /** * Fluent method to specify the collection. * * @param the entity type to use for the results. */ - interface UpsertByIdInCollection extends UpsertByIdWithOptions, InCollection { + interface UpsertByIdInCollection extends UpsertByIdWithExpiry, InCollection { /** * With a different collection * * @param collection the collection to use. */ @Override - UpsertByIdWithOptions inCollection(String collection); + UpsertByIdWithExpiry inCollection(String collection); } /** @@ -116,25 +129,11 @@ interface UpsertByIdInScope extends UpsertByIdInCollection, InScope inScope(String scope); } - interface UpsertByIdWithDurability extends UpsertByIdInScope, WithDurability { - @Override - UpsertByIdInScope withDurability(DurabilityLevel durabilityLevel); - - @Override - UpsertByIdInScope withDurability(PersistTo persistTo, ReplicateTo replicateTo); - - } - - interface UpsertByIdWithExpiry extends UpsertByIdWithDurability, WithExpiry { - @Override - UpsertByIdWithDurability withExpiry(Duration expiry); - } - /** * Provides methods for constructing KV operations in a fluent way. * * @param the entity type to upsert */ - interface ReactiveUpsertById extends UpsertByIdWithExpiry {} + interface ReactiveUpsertById extends UpsertByIdInScope {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index e84fe1fca..32949e2c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -28,6 +28,7 @@ import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; +import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -63,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; @@ -79,21 +80,25 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, null, domainType); LOG.trace("upsertById {}", pArgs); - return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) - .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + Mono tmpl = template.doGetTemplate(); + 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) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); } @Override @@ -113,7 +118,7 @@ public TerminatingUpsertById withOptions(final UpsertOptions options) { } @Override - public UpsertByIdWithDurability inCollection(final String collection) { + public UpsertByIdWithExpiry inCollection(final String collection) { return new ReactiveUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 084b1b718..260d90419 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -15,8 +15,11 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; /** * @@ -26,15 +29,23 @@ public interface TemplateSupport { CouchbaseDocument encodeEntity(Object entityToEncode); - T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder); - T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection, TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); - T applyUpdatedId(T entity, Object id); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder); - long getCas(Object entity); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, TransactionResultHolder txResultHolder, ReactiveCouchbaseResourceHolder holder); + + Long getCas(Object entity); + + Object getId(Object entity); String getJavaNameForEntity(Class clazz); void maybeEmitEvent(CouchbaseMappingEvent event); + + Integer getTxResultHolder(T source); + + 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/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 8dff2e25f..e6eec583c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -47,6 +47,7 @@ import org.springframework.data.couchbase.core.mapping.id.IdPrefix; import org.springframework.data.couchbase.core.mapping.id.IdSuffix; import org.springframework.data.couchbase.core.query.N1qlJoin; +import org.springframework.data.couchbase.repository.TransactionResult; import org.springframework.data.mapping.Alias; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.AssociationHandler; @@ -268,6 +269,9 @@ protected R read(final CouchbasePersistentEntity entity, final CouchbaseD entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (prop.isAnnotationPresent(TransactionResult.class)) { + return; + } if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop) || isIdConstructionProperty(prop) || prop.isAnnotationPresent(N1qlJoin.class)) { return; @@ -510,6 +514,10 @@ protected void writeInternal(final Object source, final CouchbaseDocument target entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (prop.isAnnotationPresent(TransactionResult.class)) { + return; + } + if (prop.equals(idProperty) || (versionProperty != null && prop.equals(versionProperty))) { return; } else if (prop.isAnnotationPresent(N1qlJoin.class)) { @@ -723,7 +731,6 @@ private CouchbaseList writeCollectionInternal(final Collection source, final target.put(writeCollectionInternal(asCollection(element), new CouchbaseList(conversions.getSimpleTypeHolder()), componentType)); } else { - CouchbaseDocument embeddedDoc = new CouchbaseDocument(); writeInternal(element, embeddedDoc, componentType, false); target.put(embeddedDoc); diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index af9ed2941..00c92153b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -24,6 +24,7 @@ import org.springframework.context.EnvironmentAware; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.env.Environment; +import org.springframework.data.couchbase.repository.TransactionResult; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.util.TypeInformation; @@ -40,6 +41,7 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity, EnvironmentAware { private Environment environment; + private CouchbasePersistentProperty transactionResult; /** * Create a new entity. @@ -69,6 +71,9 @@ public void setEnvironment(Environment environment) { @Override protected CouchbasePersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull( CouchbasePersistentProperty property) { + + transactionResult = property.findAnnotation(TransactionResult.class) != null ? property : transactionResult; + if (!property.isIdProperty()) { return null; } @@ -164,4 +169,9 @@ public boolean isTouchOnRead() { return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0; } + @Override + public CouchbasePersistentProperty transactionResultProperty() { + return transactionResult; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java index ead8146ed..02566f767 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java @@ -56,6 +56,9 @@ public class CouchbaseDocument implements CouchbaseStorable { */ private int expiration; + // todo gp + public long version; + /** * Creates a completely empty {@link CouchbaseDocument}. */ diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java index f768aab9c..c73a19513 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java @@ -61,4 +61,11 @@ public interface CouchbasePersistentEntity extends PersistentEntity entry : optsJson.toMap().entrySet()) { + txOptions.raw(entry.getKey(), entry.getValue()); + } + + if (LOG.isTraceEnabled()) { + LOG.trace("query options: {}", optsJson); + } + return txOptions; + } + public static ExistsOptions buildExistsOptions(ExistsOptions options) { options = options != null ? options : ExistsOptions.existsOptions(); return options; @@ -423,4 +441,5 @@ public static String annotationString(Class annotation AnnotatedElement[] elements) { return annotationString(annotation, "value", defaultValue, elements); } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index fb1bc9daa..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 @@ -19,19 +19,23 @@ import static org.springframework.data.couchbase.core.query.OptionsBuilder.getCollectionFrom; import static org.springframework.data.couchbase.core.query.OptionsBuilder.getScopeFrom; +import com.couchbase.client.core.error.CouchbaseException; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import com.couchbase.client.core.io.CollectionIdentifier; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; public class PseudoArgs { private final OPTS options; private final String scopeName; private final String collectionName; + private final CouchbaseTransactionalOperator transactionalOperator; - public PseudoArgs(String scopeName, String collectionName, OPTS options) { + public PseudoArgs(String scopeName, String collectionName, OPTS options, CouchbaseTransactionalOperator transactionalOperator) { this.options = options; this.scopeName = scopeName; this.collectionName = collectionName; + this.transactionalOperator = transactionalOperator; } /** @@ -39,7 +43,7 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options) { * 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 @@ -47,11 +51,12 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options) { * @param domainType - entity that may have annotations */ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, - Class domainType) { + CouchbaseTransactionalOperator transactionalOperator, Class domainType) { String scopeForQuery = null; String collectionForQuery = null; OPTS optionsForQuery = null; + CouchbaseTransactionalOperator txOpForQuery = null; // 1) repository from DynamicProxy via template threadLocal - has precedence over annotation @@ -61,16 +66,20 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle scopeForQuery = threadLocal.getScope(); collectionForQuery = threadLocal.getCollection(); optionsForQuery = threadLocal.getOptions(); + //throw new RuntimeException("PseudoArgs fix me 1"); + txOpForQuery = threadLocal.getTxOp(); + //System.err.println("threadLocal: txOpForQuery: "+txOpForQuery+" session: "); } scopeForQuery = fromFirst(null, scopeForQuery, scope, getScopeFrom(domainType)); collectionForQuery = fromFirst(null, collectionForQuery, collection, getCollectionFrom(domainType)); optionsForQuery = fromFirst(null, options, optionsForQuery); + txOpForQuery = fromFirst( null, transactionalOperator, txOpForQuery , template.txOperator() ); // if a collection was specified but no scope, use the scope from the clientFactory if (collectionForQuery != null && scopeForQuery == null) { - scopeForQuery = template.getCouchbaseClientFactory().getScope().name(); + scopeForQuery = template.getScopeName(); } // specifying scope and collection = _default is not necessary and will fail if server doesn't have collections @@ -84,7 +93,11 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle this.scopeName = scopeForQuery; this.collectionName = collectionForQuery; + if( scopeForQuery != null && collectionForQuery == null){ + throw new CouchbaseException(new IllegalArgumentException("if scope is not default or null, then collection must be specified")); + } this.options = optionsForQuery; + this.transactionalOperator = txOpForQuery; } @@ -109,8 +122,16 @@ public String getCollection() { return this.collectionName; } + /** + * @return the attempt context + */ + public CouchbaseTransactionalOperator getTxOp() { + return transactionalOperator; + } + @Override public String toString() { - return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions(); + return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions()+" txOp: "+transactionalOperator; } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java b/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java new file mode 100644 index 000000000..1399235a3 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithCas.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.support; + +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * A common interface operations that take scan consistency + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithCas { + /** + * Specify scan consistency + * + * @param cas + */ + Object withCas(Long cas); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java new file mode 100644 index 000000000..b5fd14bef --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithTransaction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.support; + +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; + +/** + * Interface for operations that take distinct fields + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithTransaction { + /** + * Specify transactions + * + * @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 01e56fb32..fbbe66ad7 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -22,12 +22,13 @@ import org.springframework.data.couchbase.repository.support.DynamicInvocationHandler; import com.couchbase.client.java.CommonOptions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * The generic parameter needs to be REPO which is either a CouchbaseRepository parameterized on T,ID or a * ReactiveCouchbaseRepository parameterized on T,ID. i.e.: interface AirportRepository extends * CouchbaseRepository<Airport, String>, DynamicProxyable<AirportRepository> - * + * * @param * @author Michael Reiche */ @@ -38,40 +39,70 @@ public interface DynamicProxyable { Object getOperations(); /** - * Support for Couchbase-specific options, scope and collections The three "with" methods will return a new proxy - * instance with the specified options, scope, or collections set. The setters are called with the corresponding - * options, scope and collection to set the ThreadLocal fields on the CouchbaseOperations of the repository just - * before the call is made to the repository, and called again with 'null' just after the call is made. The repository - * method will fetch those values to use in the call. + * Support for Couchbase-specific options, scope and collections The four "with" methods will return a new proxy + * instance with the specified options, scope, collection or transactionalOperator set. The setters are called with + * the corresponding options, scope and collection to set the ThreadLocal fields on the CouchbaseOperations of the + * repository just before the call is made to the repository, and called again with 'null' just after the call is + * made. The repository method will fetch those values to use in the call. */ /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param options - the options to set on the returned repository object */ @SuppressWarnings("unchecked") default REPO withOptions(CommonOptions options) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), - this.getClass().getInterfaces(), new DynamicInvocationHandler(this, options, null, (String) null)); + this.getClass().getInterfaces(), new DynamicInvocationHandler(this, options, null, null, null)); return proxyInstance; } /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param scope - the scope to set on the returned repository object */ @SuppressWarnings("unchecked") default REPO withScope(String scope) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), - this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, scope)); + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, scope, null)); return proxyInstance; } /** + * Note that this is is always the first/only call and therefore only one of options, collection, scope or ctx is set. + * Subsequent "with" calls are processed through the DynamicInvocationHandler and sets all of those which have already + * been set. + * * @param collection - the collection to set on the returned repository object */ @SuppressWarnings("unchecked") default REPO withCollection(String collection) { REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), - this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, collection, null)); + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, collection, null, null)); + return proxyInstance; + } + + /** + * @param ctx - the transactionalOperator for transactions + */ + @SuppressWarnings("unchecked") + /* + default REPO withTransaction(TransactionalOperator ctx) { + REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, null, ctx)); + return proxyInstance; + } + */ + + default REPO withTransaction(CouchbaseTransactionalOperator ctx) { + REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, null, ctx)); return proxyInstance; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java index db41820f9..bf8707e99 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java @@ -32,4 +32,5 @@ public interface ReactiveCouchbaseRepository extends ReactiveSortingRepos ReactiveCouchbaseOperations getOperations(); CouchbaseEntityInformation getEntityInformation(); + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/TransactionMeta.java b/src/main/java/org/springframework/data/couchbase/repository/TransactionMeta.java new file mode 100644 index 000000000..ae33ddefa --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/TransactionMeta.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * Indicates the field should hold key to lookup the TransactionGetResult and should NOT be considered part of the + * document. + * + * @author Michael Reiche + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +@Documented +@QueryAnnotation +public @interface TransactionMeta { + + String value() default ""; + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java b/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java new file mode 100644 index 000000000..d2236d520 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/TransactionResult.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * Indicates the field should hold Transaction*Result and should NOT be considered part of the document. + * + * @author Michael Reiche + */ +// todo gp can we give this a different name since there is an existing TransactionResult +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +@Documented +@QueryAnnotation +public @interface TransactionResult { + + String value() default ""; + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java index dda26c076..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; } @@ -51,12 +51,21 @@ public CouchbaseEntityInformation getEntityInformation() { return entityInformation; } + /** + * Returns the repository interface + * + * @return the underlying entity information. + */ + public Class getRepositoryInterface() { + return repositoryInterface; + } + Class getJavaType() { return getEntityInformation().getJavaType(); } String getId(S entity) { - return getEntityInformation().getId(entity); + return String.valueOf(getEntityInformation().getId(entity)); } /** diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java index 0e5dd19bf..e37636b70 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java @@ -168,10 +168,10 @@ public Object invoke(MethodInvocation invocation) throws Throwable { try { return invocation.proceed(); } finally { - // TransactionSynchronizationManager.unbindResource(method); + TransactionSynchronizationManager.unbindResource(method); } } finally { - // currentInvocation.set(oldInvocation); + currentInvocation.set(oldInvocation); } } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java index dedba009d..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 @@ -29,6 +29,7 @@ import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import com.couchbase.client.java.CommonOptions; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; /** * Invocation Handler for scope/collection/options proxy for repositories @@ -44,25 +45,40 @@ public class DynamicInvocationHandler implements InvocationHandler { final ReactiveCouchbaseTemplate reactiveTemplate; CommonOptions options; String collection; - String scope;; + String scope; + CouchbaseTransactionalOperator ctx; - public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope) { + public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope, + CouchbaseTransactionalOperator ctx) { this.target = target; if (target instanceof CouchbaseRepository) { reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); this.entityInformation = ((CouchbaseRepository) target).getEntityInformation(); } else if (target instanceof ReactiveCouchbaseRepository) { - reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) target).getOperations(); - this.entityInformation = ((ReactiveCouchbaseRepository) target).getEntityInformation(); + reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) this.target).getOperations(); + this.entityInformation = ((ReactiveCouchbaseRepository) this.target).getEntityInformation(); } else { + if( CouchbaseRepository.class.isAssignableFrom(target.getClass())) + System.err.println("isAssignable"); + printInterfaces(target.getClass(), " "); throw new RuntimeException("Unknown target type: " + target.getClass()); } this.options = options; this.collection = collection; this.scope = scope; + this.ctx = ctx; this.repositoryClass = target.getClass(); } + void printInterfaces(Class clazz, String tab){ + System.out.println(tab+"{"); + for(Class c:clazz.getInterfaces()){ + System.out.println(tab+" " +c.getSimpleName()); + if(c.getInterfaces().length > 0) + printInterfaces(c, tab+" "); + } + System.out.println(tab+"}"); + } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { @@ -75,17 +91,22 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (method.getName().equals("withOptions")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, (CommonOptions) args[0], collection, scope)); + new DynamicInvocationHandler<>(target, (CommonOptions) args[0], collection, scope, ctx)); } if (method.getName().equals("withScope")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, options, collection, (String) args[0])); + new DynamicInvocationHandler<>(target, options, collection, (String) args[0], ctx)); } if (method.getName().equals("withCollection")) { return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), - new DynamicInvocationHandler<>(target, options, (String) args[0], scope)); + new DynamicInvocationHandler<>(target, options, (String) args[0], scope, ctx)); + } + + if (method.getName().equals("withTransaction")) { + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), + new DynamicInvocationHandler<>(target, options, collection, scope, (CouchbaseTransactionalOperator) args[0])); } Class[] paramTypes = null; @@ -117,7 +138,7 @@ private void setThreadLocal() { if (reactiveTemplate.getPseudoArgs() != null) { throw new RuntimeException("pseudoArgs not yet consumed by previous caller"); } - reactiveTemplate.setPseudoArgs(new PseudoArgs(this.scope, this.collection, this.options)); + reactiveTemplate.setPseudoArgs(new PseudoArgs(this.scope, this.collection, this.options, this.ctx)); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java index 8c5a48226..7ae667acf 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java @@ -151,14 +151,14 @@ public void deleteAll(Iterable entities) { @Override public long count() { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).count(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).count(); } @Override public void deleteAll() { - operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).all(); + operations.removeByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).all(); } @Override @@ -189,8 +189,8 @@ public Page findAll(Pageable pageable) { * @return the list of found entities, already executed. */ private List findAll(Query query) { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).matching(query).all(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()).matching(query) + .withConsistency(buildQueryScanConsistency()).all(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 4d5a3c0e6..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; } @@ -71,6 +71,24 @@ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation e @SuppressWarnings("unchecked") @Override public Mono save(S entity) { + return save(entity, getScope(), getCollection()); + } + + @Override + public Flux findAll(Sort sort) { + return findAll(new Query().with(sort)); + } + + @Override + public Flux saveAll(Iterable entities) { + Assert.notNull(entities, "The given Iterable of entities must not be null!"); + String scopeName = getScope(); + String collection = getCollection(); + return Flux.fromIterable(entities).flatMap(e -> save(e, scopeName, collection)); + } + + @SuppressWarnings("unchecked") + public Mono save(S entity, String scopeName, String collectionName) { Assert.notNull(entity, "Entity must not be null!"); Mono result; final CouchbasePersistentEntity mapperEntity = operations.getConverter().getMappingContext() @@ -83,31 +101,20 @@ public Mono save(S entity) { if (!versionPresent) { // the entity doesn't have a version property // No version field - no cas - result = (Mono) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + result = (Mono) operations.upsertById(getJavaType()).inScope(scopeName).inCollection(collectionName) .one(entity); } else if (existingDocument) { // there is a version property, and it is non-zero // Updating existing document with cas - result = (Mono) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + result = (Mono) operations.replaceById(getJavaType()).inScope(scopeName).inCollection(collectionName) .one(entity); } else { // there is a version property, but it's zero or not set. // Creating new document - result = (Mono) operations.insertById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + result = (Mono) operations.insertById(getJavaType()).inScope(scopeName).inCollection(collectionName) .one(entity); } return result; } - @Override - public Flux findAll(Sort sort) { - return findAll(new Query().with(sort)); - } - - @Override - public Flux saveAll(Iterable entities) { - Assert.notNull(entities, "The given Iterable of entities must not be null!"); - return Flux.fromIterable(entities).flatMap(e -> save(e)); - } - @Override public Flux saveAll(Publisher entityStream) { Assert.notNull(entityStream, "The given Iterable of entities must not be null!"); @@ -172,7 +179,7 @@ public Mono deleteById(Publisher publisher) { @Override public Mono delete(T entity) { Assert.notNull(entity, "Entity must not be null!"); - return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(getId(entity)) + return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).oneEntity(entity) .then(); } @@ -185,7 +192,7 @@ public Mono deleteAllById(Iterable ids) { @Override public Mono deleteAll(Iterable entities) { return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) - .all(Streamable.of(entities).map(this::getId).toList()).then(); + .allEntities((java.util.Collection)(Streamable.of(entities).toList())).then(); } @Override @@ -196,19 +203,19 @@ public Mono deleteAll(Publisher entityStream) { @Override public Mono count() { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).count(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).count(); } @Override public Mono deleteAll() { - return operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).all().then(); + return operations.removeByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .withConsistency(buildQueryScanConsistency()).all().then(); } private Flux findAll(Query query) { - return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) - .inCollection(getCollection()).matching(query).all(); + return operations.findByQuery(getJavaType()).inScope(getScope()).inCollection(getCollection()).matching(query) + .withConsistency(buildQueryScanConsistency()).all(); } @Override @@ -216,4 +223,26 @@ public ReactiveCouchbaseOperations getOperations() { return operations; } + /** + * Get the TransactionalOperator from
+ * 1. The template.clientFactory
+ * 2. The template.threadLocal
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() + */ + /* + private CouchbaseStuffHandle getTransactionalOperator() { + if (operations.getCouchbaseClientFactory().getTransactionalOperator() != null) { + return operations.getCouchbaseClientFactory().getTransactionalOperator(); + } + ReactiveCouchbaseTemplate t = (ReactiveCouchbaseTemplate) operations; + PseudoArgs pArgs = t.getPseudoArgs(); + if (pArgs != null && pArgs.getTxOp() != null) { + return pArgs.getTxOp(); + } + return null; + } + */ + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java new file mode 100644 index 000000000..653bba57f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/TransactionResultHolder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository.support; + +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.transactions.TransactionGetResult; +import reactor.util.annotation.Nullable; + +/** + * Holds previously obtained Transaction*Result + * + * @author Michael Reiche + */ +public class TransactionResultHolder { + + private final @Nullable CoreTransactionGetResult getResult; + // todo gp needed? + private final @Nullable QueryResult singleQueryResult; + + public TransactionResultHolder(CoreTransactionGetResult getResult) { + // we don't need the content and we don't have access to the transcoder an txnMeta (and we don't need them either). + // todo gp will need to expose a copy ctor if a copy is needed + this.getResult = getResult; +// this.getResult = new TransactionGetResult(getResult.id(), null, getResult.cas(), getResult.collection(), +// getResult.links(), getResult.status(), getResult.documentMetadata(), null, null); + this.singleQueryResult = null; + } + + public TransactionResultHolder(QueryResult singleQueryResult) { + this.getResult = null; + this.singleQueryResult = singleQueryResult; + } + + public @Nullable CoreTransactionGetResult transactionGetResult() { + return getResult; + } + + public @Nullable QueryResult singleQueryResult() { + return singleQueryResult; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java new file mode 100644 index 000000000..19ae0a921 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseCallbackTransactionManager.java @@ -0,0 +1,296 @@ +///* +// * Copyright 2021 the original author or authors +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +//package org.springframework.data.couchbase.transaction; +// +//import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +//import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +//import com.couchbase.client.java.transactions.TransactionResult; +//import reactor.core.publisher.Mono; +// +//import java.time.Duration; +//import java.time.temporal.ChronoUnit; +//import java.util.concurrent.atomic.AtomicReference; +// +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.DisposableBean; +//import org.springframework.data.couchbase.CouchbaseClientFactory; +//import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +//import org.springframework.data.couchbase.core.CouchbaseTemplate; +//import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +//import org.springframework.transaction.TransactionDefinition; +//import org.springframework.transaction.TransactionException; +//import org.springframework.transaction.reactive.TransactionContextManager; +//import org.springframework.transaction.reactive.TransactionSynchronizationManager; +//import org.springframework.transaction.support.AbstractPlatformTransactionManager; +//import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +//import org.springframework.transaction.support.DefaultTransactionStatus; +//import org.springframework.transaction.support.ResourceTransactionManager; +//import org.springframework.transaction.support.SmartTransactionObject; +//import org.springframework.transaction.support.TransactionCallback; +//import org.springframework.transaction.support.TransactionSynchronizationUtils; +//import org.springframework.util.Assert; +// +///** +// * Blocking TransactionManager +// * +// * @author Michael Nitschinger +// * @author Michael Reiche +// */ +// +//public class CouchbaseCallbackTransactionManager extends AbstractPlatformTransactionManager +// implements DisposableBean, ResourceTransactionManager, CallbackPreferringPlatformTransactionManager { +// +// private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); +// +// private final CouchbaseTemplate template; +// private final ReactiveCouchbaseTemplate reactiveTemplate; +// private final ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; +// private final CouchbaseClientFactory couchbaseClientFactory; +// +// private ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction; +// +// public CouchbaseCallbackTransactionManager(CouchbaseTemplate template, ReactiveCouchbaseTemplate reactiveTemplate) { +// this.template = template; +// this.reactiveTemplate = reactiveTemplate; +// this.reactiveCouchbaseClientFactory = this.reactiveTemplate.getCouchbaseClientFactory(); +// this.couchbaseClientFactory = this.template.getCouchbaseClientFactory(); +// } +// +// public ReactiveCouchbaseTemplate template() { +// return reactiveTemplate; +// } +// +// private CouchbaseResourceHolder newResourceHolder(TransactionDefinition definition, ClientSessionOptions options, +// ReactiveTransactionAttemptContext atr) { +// +// CouchbaseClientFactory databaseFactory = template.getCouchbaseClientFactory(); +// +// CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder( +// databaseFactory.getSession(options, atr), databaseFactory); +// return resourceHolder; +// } +// +// @Override +// public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { +// final AtomicReference execResult = new AtomicReference<>(); +// AtomicReference startTime = new AtomicReference<>(0L); +// +// Mono txnResult = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { +// /* begin spring-data-couchbase transaction 1/2 */ +// ClientSession clientSession = reactiveCouchbaseClientFactory // couchbaseClientFactory +// .getSession(ClientSessionOptions.builder().causallyConsistent(true).build()) +// .block(); +// ReactiveCouchbaseResourceHolder reactiveResourceHolder = new ReactiveCouchbaseResourceHolder(clientSession, +// reactiveCouchbaseClientFactory); +// +// CouchbaseResourceHolder resourceHolder = new CouchbaseResourceHolder(clientSession, +// template.getCouchbaseClientFactory()); +// +// Mono sync = TransactionContextManager.currentContext() +// .map(TransactionSynchronizationManager::new) +// . flatMap(synchronizationManager -> { +// System.err.println("CallbackTransactionManager: " + this); +// System.err.println("bindResource: " + reactiveCouchbaseClientFactory.getCluster().block()); +// // todo gp not sure why we bind, unbind, bind again? +// // todo msr - to avoid the NotBound exception on unbind. Should use unbindIfPossible. +// synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), +// reactiveResourceHolder); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .bindResource(reactiveCouchbaseClientFactory.getCluster().block(), resourceHolder); +// ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction = new ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject( +// reactiveResourceHolder); +// setTransaction(transaction); +// +// // todo gp experimenting with replacing the ClientSession, the ReactiveCouchbaseTransactionObject, +// // the resource holders etc., with just storing the TransactionAttemptContext. +// synchronizationManager.bindResource(ReactiveTransactionAttemptContext.class, ctx); +// +// /* end spring-data-couchbase transaction 1/2 */ +// +// // todo gp do we need TransactionSynchronizationManager.forCurrentTransaction()? as we already have synchronizationManager +// Mono result = TransactionSynchronizationManager.forCurrentTransaction().flatMap((sm) -> { +// // todo gp not sure why re-binding again? +// sm.unbindResourceIfPossible(reactiveCouchbaseClientFactory.getCluster().block()); +// sm.bindResource(reactiveCouchbaseClientFactory.getCluster().block(), +// reactiveResourceHolder); +// CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(transaction, true, false, false, true, null, sm); +// prepareSynchronization(status, new CouchbaseTransactionDefinition()); +// // System.err.println("deferContextual.ctx : " + xxx); +// //Mono cxView = Mono.deferContextual(cx -> { System.err.println("CallbackTransactionManager.cx: "+cx); return Mono.just(cx);}); +// try { +// // Since we are on a different thread now transparently, at least make sure +// // that the original method invocation is synchronized. +// synchronized (this) { +// // todo gp this will execute the lambda, and so we likely don't want that to be inside a synchronized block +// execResult.set(callback.doInTransaction(status)); +// } +// } catch (RuntimeException e) { +// throw e; +// } catch (Throwable e) { +// throw new RuntimeException(e); +// } +// return Mono.empty(); +// }).contextWrite(TransactionContextManager.getOrCreateContext()) // this doesn't create a context on the desired publisher +// .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); +// +// // todo gp this isn't part of the chain (no `result = result.onErrorResume...`) so isn't called +// // and presumably isn't needed? +//// result.onErrorResume(err -> { +//// AttemptContextReactiveAccessor.getLogger(ctx).info(ctx.attemptId(), +//// "caught exception '%s' in async, rethrowing", err); +//// return Mono.error(ctx.TransactionOperationFailedException.convertToOperationFailedIfNeeded(err, ctx)); +//// }).thenReturn(ctx); +// +// return result.then(Mono.just(synchronizationManager)); +// }); +// /* begin spring-data-couchbase transaction 2/2 */ // this doesn't create a context on the desired publisher +// return sync.contextWrite(TransactionContextManager.getOrCreateContext()) +// .contextWrite(TransactionContextManager.getOrCreateContextHolder()).then(); +// /* end spring-data-couchbase transaction 2/2 */ +// }).doOnSubscribe(v -> startTime.set(System.nanoTime())); +// +// txnResult.block(); +// return execResult.get(); // transactions.reactive().executeTransaction(merged,overall,ob).doOnNext(v->overall.span().finish()).doOnError(err->overall.span().failWith(err));}); +// +// } +// +// private void setTransaction(ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject transaction) { +// this.transaction = transaction; +// } +// +// @Override +// protected ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject doGetTransaction() +// throws TransactionException { +// /* +// CouchbaseResourceHolder resourceHolder = (CouchbaseResourceHolder) TransactionSynchronizationManager +// .getResource(template.getCouchbaseClientFactory()); +// return new CouchbaseTransactionManager.CouchbaseTransactionObject(resourceHolder); +// */ +// return (ReactiveCouchbaseTransactionManager.ReactiveCouchbaseTransactionObject) transaction; +// } +// +// @Override +// protected boolean isExistingTransaction(Object transaction) throws TransactionException { +// return extractTransaction(transaction).hasResourceHolder(); +// } +// +// @Override +// protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { +// LOGGER.debug("Beginning Couchbase Transaction with Definition {}", definition); +// } +// +// @Override +// protected void doCommit(DefaultTransactionStatus status) throws TransactionException { +// LOGGER.debug("Committing Couchbase Transaction with status {}", status); +// } +// +// @Override +// protected void doRollback(DefaultTransactionStatus status) throws TransactionException { +// LOGGER.warn("Rolling back Couchbase Transaction with status {}", status); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .unbindResource(reactiveCouchbaseClientFactory); +// } +// +// @Override +// protected void doCleanupAfterCompletion(Object transaction) { +// LOGGER.trace("Performing cleanup of Couchbase Transaction {}", transaction); +// org.springframework.transaction.support.TransactionSynchronizationManager +// .unbindResource(reactiveCouchbaseClientFactory); +// return; +// } +// +// @Override +// public void destroy() { +// } +// +// @Override +// public Object getResourceFactory() { +// return reactiveTemplate.getCouchbaseClientFactory(); +// } +// +// private static CouchbaseTransactionObject extractTransaction(Object transaction) { +// Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, +// () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, +// transaction.getClass())); +// +// return (CouchbaseTransactionObject) transaction; +// } +// /* +// public class CouchbaseResourceHolder extends ResourceHolderSupport { +// +// private volatile ReactiveTransactionAttemptContext attemptContext; +// //private volatile TransactionResultMap resultMap = new TransactionResultMap(template); +// +// public CouchbaseResourceHolder(ReactiveTransactionAttemptContext attemptContext) { +// this.attemptContext = attemptContext; +// } +// +// public ReactiveTransactionAttemptContext getAttemptContext() { +// return attemptContext; +// } +// +// public void setAttemptContext(ReactiveTransactionAttemptContext attemptContext) { +// this.attemptContext = attemptContext; +// } +// +// //public TransactionResultMap getTxResultMap() { +// // return resultMap; +// //} +// +// @Override +// public String toString() { +// return "CouchbaseResourceHolder{" + "attemptContext=" + attemptContext + "}"; +// } +// } +// +// */ +// +// protected static class CouchbaseTransactionObject implements SmartTransactionObject { +// +// private final CouchbaseResourceHolder resourceHolder; +// +// CouchbaseTransactionObject(CouchbaseResourceHolder resourceHolder) { +// this.resourceHolder = resourceHolder; +// } +// +// @Override +// public boolean isRollbackOnly() { +// return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); +// } +// +// @Override +// public void flush() { +// TransactionSynchronizationUtils.triggerFlush(); +// } +// +// public boolean hasResourceHolder() { +// return resourceHolder != null; +// } +// +// @Override +// public String toString() { +// return "CouchbaseTransactionObject{" + "resourceHolder=" + resourceHolder + '}'; +// } +// } +// +// private static Duration now() { +// return Duration.of(System.nanoTime(), ChronoUnit.NANOS); +// } +// +//} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolderx.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolderx.java new file mode 100644 index 000000000..a4ba04574 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseResourceHolderx.java @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.transaction; + +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; + +/** + * MongoDB specific resource holder, wrapping a {@link CoreTransactionAttemptContext}. + * {@link ReactiveCouchbaseTransactionManager} binds instances of this class to the subscriber context. + *

+ * Note: Intended for internal usage only. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.2 + * @see CouchbaseTransactionManager + * @see CouchbaseTemplate + */ +// todo gp understand why this is needed - can we not just hold ctx in Mono context? +public class CouchbaseResourceHolderx extends ResourceHolderSupport { + + private @Nullable CoreTransactionAttemptContext core; // which holds the atr + private CouchbaseClientFactory databaseFactory; + + /** + * Create a new {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} for a given + * {@link CoreTransactionAttemptContext session}. + * + * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + * @param databaseFactory the associated {@link CouchbaseClientFactory}. must not be {@literal null}. + */ + public CouchbaseResourceHolderx(@Nullable CoreTransactionAttemptContext core, CouchbaseClientFactory databaseFactory) { + + this.core = core; + this.databaseFactory = databaseFactory; + } + + /** + * @return the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + @Nullable + public CoreTransactionAttemptContext getCore() { + return core; + } + + /** + * @return the required associated {@link CoreTransactionAttemptContext}. + * @throws IllegalStateException if no session is associated. + */ + CoreTransactionAttemptContext getRequiredSession() { + + CoreTransactionAttemptContext session = getCore(); + + if (session == null) { + throw new IllegalStateException("No ClientSession associated"); + } + return session; + } + + /** + * @return the associated {@link CouchbaseClientFactory}. + */ + public CouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + /** + * Set the {@link CoreTransactionAttemptContext} to guard. + * + * @param core can be {@literal null}. + */ + public void setCore(@Nullable CoreTransactionAttemptContext core) { + this.core = core; + } + + /** + * @return {@literal true} if session is not {@literal null}. + */ + boolean hasCore() { + return core != null; + } + + /** + * If the {@link org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder} is {@link #hasCore()} + * not already associated} with a {@link CoreTransactionAttemptContext} the given value is + * {@link #setCore(CoreTransactionAttemptContext) set} and returned, otherwise the current bound session is returned. + * + * @param core + * @return + */ + @Nullable + public CoreTransactionAttemptContext setSessionIfAbsent(@Nullable CoreTransactionAttemptContext core) { + + if (!hasCore()) { + setCore(core); + } + + return core; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java new file mode 100644 index 000000000..772ed2853 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleCallbackTransactionManager.java @@ -0,0 +1,141 @@ +/* + * Copyright 2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.lang.Nullable; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.InvalidTimeoutException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; + +public class CouchbaseSimpleCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionManager.class); + + private final ReactiveCouchbaseClientFactory couchbaseClientFactory; + private TransactionOptions options; + + public CouchbaseSimpleCallbackTransactionManager(ReactiveCouchbaseClientFactory couchbaseClientFactory, TransactionOptions options) { + this.couchbaseClientFactory = couchbaseClientFactory; + this.options = options; + } + + @Override + public T execute(TransactionDefinition definition, TransactionCallback callback) throws TransactionException { + final AtomicReference execResult = new AtomicReference<>(); + + setOptionsFromDefinition(definition); + + TransactionResult result = couchbaseClientFactory.getCluster().block().transactions().run(ctx -> { + CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(null, true, false, false, true, null, null); + + populateTransactionSynchronizationManager(ctx); + + try { + execResult.set(callback.doInTransaction(status)); + } + finally { + TransactionSynchronizationManager.clear(); + } + }, this.options); + + 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? + } + + } + + // 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); + } + + /** + * Test transaction infrastructure uses this to determine if transaction is active + * + * @param definition + * @return + * @throws TransactionException + */ + @Override + public TransactionStatus getTransaction(@Nullable TransactionDefinition definition) + throws TransactionException { + TransactionStatus status = new DefaultTransactionStatus( null, true, true, + false, true, false); + return status; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + // 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); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleTransactionManager.java new file mode 100644 index 000000000..1947cb7a2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseSimpleTransactionManager.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.transaction; + +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionSystemException; + +// todo gp experimenting with the simplest possible class, extending PlatformTransactionManager not AbstractPlatformTransactionManager +public class CouchbaseSimpleTransactionManager implements PlatformTransactionManager { + + private final CouchbaseClientFactory clientFactory; + + public CouchbaseSimpleTransactionManager(CouchbaseClientFactory clientFactory) { + this.clientFactory = clientFactory; + } + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + return null; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + // todo gp what here - do we need to re-allow explicit commit? how to handle retries of this part? + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + // todo gp same as commit() + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java new file mode 100644 index 000000000..d46d73bc7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionDefinition.java @@ -0,0 +1,37 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +public class CouchbaseTransactionDefinition extends DefaultTransactionDefinition { + + ReactiveTransactionAttemptContext atr; + TransactionAttemptContext at; + + public CouchbaseTransactionDefinition(){ + super(); + setIsolationLevel(ISOLATION_READ_COMMITTED); + } + + public CouchbaseTransactionDefinition(TransactionDefinition that) { + super(that); + } + + public CouchbaseTransactionDefinition(int propagationBehavior) { + super(propagationBehavior); + } + + public void setAttemptContextReactive(ReactiveTransactionAttemptContext atr){ + this.atr = atr; + } + + public ReactiveTransactionAttemptContext getAttemptContextReactive(){ + return atr; + } + + public void setAttemptContext(TransactionAttemptContext attemptContext) { + at = attemptContext; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java new file mode 100644 index 000000000..c5dc26b62 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionManager.java @@ -0,0 +1,507 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionAttemptContext; +import com.couchbase.client.java.transactions.Transactions; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.couchbase.client.core.error.CouchbaseException; +import reactor.core.publisher.Mono; + +/** + * A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages + * {@link CoreTransactionAttemptContext} based transactions for a single {@link CouchbaseClientFactory}. + *

+ * 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. + *

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

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

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

+ * TODO: Optionally it is possible to set default {@link TransactionOptions transaction options} defining TODO: + * {@link ReadConcern} and {@link WriteConcern}. + * + * @see #setDbFactory(CouchbaseClientFactory) + * @see #setTransactionSynchronization(int) + */ + public CouchbaseTransactionManager() {} + + /** + * Create a new {@link CouchbaseTransactionManager} obtaining sessions from the given {@link CouchbaseClientFactory} + * applying the given {@link TransactionOptions options}, if present, when starting a new transaction. + * + * @param databaseFactory must not be {@literal null}. @//param options can be {@literal null}. + */ + public CouchbaseTransactionManager(CouchbaseClientFactory databaseFactory, @Nullable TransactionOptions options) { + + Assert.notNull(databaseFactory, "DbFactory must not be null!"); + System.err.println(this); + System.err.println(databaseFactory.getCluster()); + this.databaseFactory = databaseFactory; + this.options = options; + this.transactions = databaseFactory.getCluster().transactions(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction() + */ + @Override + protected Object doGetTransaction() throws TransactionException { + ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) TransactionSynchronizationManager + .getResource(getRequiredDatabaseFactory().getCluster()); + return new CouchbaseTransactionObject(resourceHolder); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#isExistingTransaction(java.lang.Object) + */ + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractCouchbaseTransaction(transaction).hasResourceHolder(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doBegin(java.lang.Object, org.springframework.transaction.TransactionDefinition) + */ + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); +// should ACR already be in TSM? TransactionSynchronizationManager.bindResource(getRequiredDbFactory().getCluster(), resourceHolder); + 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()))); + } + + try { + couchbaseTransactionObject.startTransaction(options); + } catch (CouchbaseException ex) { + throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.", + debugString(couchbaseTransactionObject.getCore())), ex); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getCore()))); + } + + TransactionSynchronizationManager.setActualTransactionActive(true); + resourceHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.unbindResourceIfPossible(getRequiredDatabaseFactory().getCluster()); + System.err.println("CouchbaseTransactionManager: " + this); + System.err.println("bindResource: " + getRequiredDatabaseFactory().getCluster() + " value: " + resourceHolder); + TransactionSynchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster(), resourceHolder); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doSuspend(java.lang.Object) + */ + @Override + protected Object doSuspend(Object transaction) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); + couchbaseTransactionObject.setResourceHolder(null); + + return TransactionSynchronizationManager.unbindResource(getRequiredDatabaseFactory().getCluster()); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doResume(java.lang.Object, java.lang.Object) + */ + @Override + protected void doResume(@Nullable Object transaction, Object suspendedResources) { + TransactionSynchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster(), suspendedResources); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected final void doCommit(DefaultTransactionStatus status) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to commit transaction for session %s.", + debugString(couchbaseTransactionObject.getCore()))); + } + + try { + doCommit(couchbaseTransactionObject); + } catch (Exception ex) { + 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); + } + } + + /** + * 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.
+ * + *

+	 * int retries = 3;
+	 * do {
+	 *     try {
+	 *         transactionObject.commitTransaction();
+	 *         break;
+	 *     } catch (CouchbaseException ex) {
+	 *         if (!ex.hasErrorLabel(CouchbaseException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
+	 *             throw ex;
+	 *         }
+	 *     }
+	 *     Thread.sleep(500);
+	 * } while (--retries > 0);
+	 *     
+ * + * + * @param transactionObject never {@literal null}. + * @throws Exception in case of transaction errors. + */ + protected void doCommit(CouchbaseTransactionObject transactionObject) throws Exception { + transactionObject.commitTransaction(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + + CouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to abort transaction for session %s.", + debugString(couchbaseTransactionObject.getCore()))); + } + + try { + couchbaseTransactionObject.abortTransaction(); + } catch (CouchbaseException ex) { + + throw new TransactionSystemException(String.format("Could not abort Couchbase transaction for session %s.", + debugString(couchbaseTransactionObject.getCore())), ex); + } + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doSetRollbackOnly(org.springframework.transaction.support.DefaultTransactionStatus) + */ + @Override + protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { + + CouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status); + throw new TransactionException("need to setRollbackOnly() here"){}; + //transactionObject.getRequiredResourceHolder().setRollbackOnly(); + } + + /* + * (non-Javadoc) + * org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object) + */ + @Override + protected void doCleanupAfterCompletion(Object transaction) { + + Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + transaction.getClass())); + + CouchbaseTransactionObject couchbaseTransactionObject = (CouchbaseTransactionObject) transaction; + + // Remove the connection holder from the thread. + TransactionSynchronizationManager.unbindResourceIfPossible(getRequiredDatabaseFactory().getCluster()); + //couchbaseTransactionObject.getRequiredResourceHolder().clear(); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to release Core %s after transaction.", + debugString(couchbaseTransactionObject.getCore()))); + } + + couchbaseTransactionObject.closeSession(); + } + + /** + * Set the {@link CouchbaseClientFactory} that this instance should manage transactions for. + * + * @param databaseFactory must not be {@literal null}. + */ + public void setDbFactory(CouchbaseClientFactory databaseFactory) { + + Assert.notNull(databaseFactory, "DbFactory must not be null!"); + this.databaseFactory = databaseFactory; + } + + /** + * Set the {@link TransactionOptions} to be applied when starting transactions. + * + * @param options can be {@literal null}. + */ + public void setOptions(@Nullable TransactionOptions options) { + this.options = options; + } + + /** + * Get the {@link CouchbaseClientFactory} that this instance manages transactions for. + * + * @return can be {@literal null}. + */ + @Nullable + public CouchbaseClientFactory getDbFactory() { + return databaseFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceTransactionManager#getResourceFactory() + */ + @Override + public CouchbaseClientFactory getResourceFactory() { + return getRequiredDatabaseFactory(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + getRequiredDatabaseFactory(); + } + + static ReactiveCouchbaseResourceHolder newResourceHolder(CouchbaseClientFactory databaseFactory, TransactionDefinition definition, + TransactionOptions options, CoreTransactionAttemptContext atr) { + ReactiveCouchbaseResourceHolder resourceHolder = new ReactiveCouchbaseResourceHolder( + databaseFactory.getCore(options, atr)); + return resourceHolder; + } + + /** + * @throws IllegalStateException if {@link #databaseFactory} is {@literal null}. + */ + private CouchbaseClientFactory getRequiredDatabaseFactory() { + + Assert.state(databaseFactory != null, + "MongoTransactionManager operates upon a MongoDbFactory. Did you forget to provide one? It's required."); + + return databaseFactory; + } + + private static CouchbaseTransactionObject extractCouchbaseTransaction(Object transaction) { + + Assert.isInstanceOf(CouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + transaction.getClass())); + + return (CouchbaseTransactionObject) transaction; + } + + private static CouchbaseTransactionObject extractCouchbaseTransaction(DefaultTransactionStatus status) { + + Assert.isInstanceOf(CouchbaseTransactionObject.class, status.getTransaction(), + () -> String.format("Expected to find a %s but it turned out to be %s.", CouchbaseTransactionObject.class, + status.getTransaction().getClass())); + + return (CouchbaseTransactionObject) status.getTransaction(); + } + + static String debugString(@Nullable CoreTransactionAttemptContext ctx) { + if (ctx == null) { + return "null"; + } + String debugString = String.format("[%s@%s ", ClassUtils.getShortName(ctx.getClass()), + Integer.toHexString(ctx.hashCode())); + try { + debugString += String.format("core=%s", ctx); + } catch (RuntimeException e) { + debugString += String.format("error = %s", e.getMessage()); + } + debugString += "]"; + return debugString; + } + + public CouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + /** + * MongoDB specific transaction object, representing a {@link ReactiveCouchbaseResourceHolder}. Used as transaction + * object by {@link CouchbaseTransactionManager}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + * @see ReactiveCouchbaseResourceHolder + */ + protected static class CouchbaseTransactionObject implements SmartTransactionObject { + + private @Nullable ReactiveCouchbaseResourceHolder resourceHolder; + + CouchbaseTransactionObject(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * Set the {@link ReactiveCouchbaseResourceHolder}. + * + * @param resourceHolder can be {@literal null}. + */ + void setResourceHolder(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * @return {@literal true} if a {@link ReactiveCouchbaseResourceHolder} is set. + */ + final boolean hasResourceHolder() { + return resourceHolder != null; + } + + /** + * Start a MongoDB transaction optionally given {@link TransactionOptions}. + * + * @param options can be {@literal null} + */ + void startTransaction(TransactionOptions options) { + + CoreTransactionAttemptContext core = getRequiredCore(); + // if (options != null) { + // session.startTransaction(options); + // } else { + //core.startTransaction(); + // } + } + + /** + * Commit the transaction. + */ + public void commitTransaction() { + getRequiredCore().commit().block(); + } + + /** + * Rollback (abort) the transaction. + */ + public void abortTransaction() { + getRequiredCore().rollback().block(); + } + + /** + * Close a {@link CoreTransactionAttemptContext} without regard to its transactional state. + */ + void closeSession() { + CoreTransactionAttemptContext core = getRequiredCore(); + } + + @Nullable + public CoreTransactionAttemptContext getCore() { + return resourceHolder != null ? resourceHolder.getCore() : null; + } + + private ReactiveCouchbaseResourceHolder getRequiredResourceHolder() { + Assert.state(resourceHolder != null, "CouchbaseResourceHolder is required but not present. o_O"); + return resourceHolder; + } + + private CoreTransactionAttemptContext getRequiredCore() { + CoreTransactionAttemptContext core = getCore(); + Assert.state(core != null, "A Core is required but it turned out to be null."); + return core; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#isRollbackOnly() + */ + @Override + public boolean isRollbackOnly() { + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#flush() + */ + @Override + public void flush() { + TransactionSynchronizationUtils.triggerFlush(); + } + + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java new file mode 100644 index 000000000..5331cea53 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionStatus.java @@ -0,0 +1,33 @@ +package org.springframework.data.couchbase.transaction; + +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.DefaultTransactionStatus; + +public class CouchbaseTransactionStatus extends DefaultTransactionStatus { + + final TransactionSynchronizationManager transactionSynchronizationManager; + /** + * Create a new {@code DefaultTransactionStatus} instance. + * + * @param transaction underlying transaction object that can hold state + * for the internal transaction implementation + * @param newTransaction if the transaction is new, otherwise participating + * in an existing transaction + * @param newSynchronization if a new transaction synchronization has been + * opened for the given transaction + * @param readOnly whether the transaction is marked as read-only + * @param debug should debug logging be enabled for the handling of this transaction? + * Caching it in here can prevent repeated calls to ask the logging system whether + * debug logging should be enabled. + * @param suspendedResources a holder for resources that have been suspended + */ + public CouchbaseTransactionStatus(Object transaction, boolean newTransaction, boolean newSynchronization, boolean readOnly, boolean debug, Object suspendedResources, TransactionSynchronizationManager sm) { + super(transaction, + newTransaction, + newSynchronization, + readOnly, + debug, + suspendedResources); + transactionSynchronizationManager = sm; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java new file mode 100644 index 000000000..10002be95 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/CouchbaseTransactionalOperator.java @@ -0,0 +1,231 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.core.transaction.CoreTransactionGetResult; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionGetResult; +import com.couchbase.client.java.transactions.TransactionResult; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.reactive.TransactionCallback; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionalOperator; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.util.Assert; + +import com.couchbase.client.core.error.CouchbaseException; + +/** + * What's this for again? + * A transaction-enabled operator that uses the CouchbaseStuffHandle txOp instead of + * what it finds in the currentContext()? + * + */ +public class CouchbaseTransactionalOperator implements TransactionalOperator { + + // package org.springframework.transaction.reactive; + private static final Log logger = LogFactory.getLog(CouchbaseTransactionalOperator.class); + private final ReactiveTransactionManager transactionManager; + private final TransactionDefinition transactionDefinition; + + Map getResultMap = new HashMap<>(); + private ReactiveTransactionAttemptContext attemptContextReactive; + + public CouchbaseTransactionalOperator() { + transactionManager = null; + transactionDefinition = null; + } + + public CouchbaseTransactionalOperator(ReactiveCouchbaseTransactionManager transactionManager) { + this(transactionManager, new CouchbaseTransactionDefinition()); + } + + public ReactiveCouchbaseTemplate getTemplate(){ + return ((ReactiveCouchbaseTransactionManager)transactionManager).getDatabaseFactory().getTransactionalOperator() + .getTemplate(); + } + public CouchbaseTransactionalOperator(ReactiveCouchbaseTransactionManager transactionManager, + TransactionDefinition transactionDefinition) { + Assert.notNull(transactionManager, "ReactiveTransactionManager must not be null"); + Assert.notNull(transactionDefinition, "TransactionDefinition must not be null"); + this.transactionManager = transactionManager; + this.transactionDefinition = transactionDefinition; + } + + public Mono reactive(Function> transactionLogic) { + return reactive(transactionLogic, true); + } + + public TransactionResult run(Function transactionLogic) { + return reactive(new Function>() { + @Override + public Mono apply(CouchbaseTransactionalOperator couchbaseTransactionalOperator) { + return Mono.defer(() -> {transactionLogic.apply( couchbaseTransactionalOperator); return Mono.empty();}); + } + }, + true).block(); + } + + + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default + * PerTransactionConfig. + */ + public Mono reactive(Function> transactionLogic, + boolean commit) { +// // todo gp this needs access to a Cluster +// return Mono.empty(); + return ((ReactiveCouchbaseTransactionManager) transactionManager).getDatabaseFactory().getCluster().block().reactive().transactions().run(ctx -> { + setAttemptContextReactive(ctx); // for getTxOp().getCtx() in Reactive*OperationSupport + // for transactional(), transactionDefinition.setAtr(ctx) is called at the beginning of that method + // and is eventually added to the ClientSession in transactionManager.doBegin() via newResourceHolder() + return transactionLogic.apply(this); + }/*, commit*/); + } + + public TransactionResultHolder transactionResultHolder(Integer key) { + return getResultMap.get(key); + } + + public TransactionResultHolder transactionResultHolder(CoreTransactionGetResult result) { + TransactionResultHolder holder = new TransactionResultHolder(result); + getResultMap.put(System.identityHashCode(holder), holder); + return holder; + } + + public void setAttemptContextReactive(ReactiveTransactionAttemptContext attemptContextReactive) { + this.attemptContextReactive = attemptContextReactive; + // see ReactiveCouchbaseTransactionManager.doBegin() + // transactionManager.getReactiveTransaction(new CouchbaseTransactionDefinition()).block(); + // CouchbaseResourceHolder holder = null; + //TransactionSynchronizationManager.bindResource(((ReactiveCouchbaseTransactionManager)transactionManager).getDatabaseFactory(), holder); + + /* + for savePerson that, doBegin() is called from AbstractReactiveTransactionManager.getReactiveTransaction() + which is called from TransactionalOperatorImpl.transactional(Mono) + [also called from TransactionalOperatorImpl.execute(TransactionCallback)] + */ + } + + public ReactiveTransactionAttemptContext getAttemptContextReactive() { + return attemptContextReactive; + } + + public CoreTransactionAttemptContext getAttemptContext() { + return AttemptContextReactiveAccessor.getCore(attemptContextReactive); + } + + + public ReactiveTransactionManager getTransactionManager() { + return transactionManager; + } + + public ReactiveCouchbaseTemplate template(ReactiveCouchbaseTemplate template) { + ReactiveCouchbaseTransactionManager txMgr = ((ReactiveCouchbaseTransactionManager) ((CouchbaseTransactionalOperator) this) + .getTransactionManager()); + if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Template must use the same clientFactory as the transactionManager of the transactionalOperator " + + template); + } + return template.with(this); // template with a new couchbaseClient with txOperator + } + + /* + public CouchbaseTemplate template(CouchbaseTemplate template) { + CouchbaseTransactionManager txMgr = ((CouchbaseTransactionManager) ((CouchbaseStuffHandle) this) + .getTransactionManager()); + if (template.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Template must use the same clientFactory as the transactionManager of the transactionalOperator " + + template); + } + return template.with(this); // template with a new couchbaseClient with txOperator + } +*/ + public > R repository(R repo) { + if (!(repo.getOperations() instanceof ReactiveCouchbaseOperations)) { + throw new CouchbaseException("Repository must be a Reactive Couchbase repository" + repo); + } + ReactiveCouchbaseOperations reactiveOperations = (ReactiveCouchbaseOperations) repo.getOperations(); + ReactiveCouchbaseTransactionManager txMgr = (ReactiveCouchbaseTransactionManager) this.getTransactionManager(); + + if (reactiveOperations.getCouchbaseClientFactory() != txMgr.getDatabaseFactory()) { + throw new CouchbaseException( + "Repository must use the same clientFactory as the transactionManager of the transactionalOperator " + repo); + } + return repo.withTransaction(this); // this returns a new repository proxy with txOperator in its threadLocal + // what if instead we returned a new repo with a new template with the txOperator? + } + + @Override + public Flux transactional(Flux flux) { + return execute(it -> flux); + } + + @Override + public Mono transactional(Mono mono) { + return TransactionContextManager.currentContext().flatMap(context -> { + // getCtx()/getAttemptTransActionReactive() has the atr + // atr : transactionalOpterator -> transactionDefinition -> transactionHolder -> + ((CouchbaseTransactionDefinition) transactionDefinition).setAttemptContextReactive(getAttemptContextReactive()); + Mono status = this.transactionManager.getReactiveTransaction(this.transactionDefinition); + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + // Need re-wrapping of ReactiveTransaction until we get hold of the exception + // through usingWhen. + return status + .flatMap(it -> Mono + .usingWhen(Mono.just(it), ignore -> mono, this.transactionManager::commit, (res, err) -> { System.err.println("!!!!!!!!!! "+err+" "+res); return Mono.empty();}, + this.transactionManager::rollback) + .onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex)))); + }).contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + } + + @Override + public Flux execute(TransactionCallback action) throws TransactionException { + return TransactionContextManager.currentContext().flatMapMany(context -> { + Mono status = this.transactionManager.getReactiveTransaction(this.transactionDefinition); + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + // Need re-wrapping of ReactiveTransaction until we get hold of the exception + // through usingWhen. + return status + .flatMapMany(it -> Flux + .usingWhen(Mono.just(it), action::doInTransaction, this.transactionManager::commit, + (tx, ex) -> Mono.empty(), this.transactionManager::rollback) + .onErrorResume(ex -> rollbackOnException(it, ex).then(Mono.error(ex)))); + }).contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + } + + private Mono rollbackOnException(ReactiveTransaction status, Throwable ex) throws TransactionException { + logger.debug("Initiating transaction rollback on application exception", ex); + return this.transactionManager.rollback(status).onErrorMap((ex2) -> { + logger.error("Application exception overridden by rollback exception", ex); + if (ex2 instanceof TransactionSystemException) { + ((TransactionSystemException) ex2).initApplicationException(ex); + } + return ex2; + }); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java new file mode 100644 index 000000000..31865f1b0 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseClientUtils.java @@ -0,0 +1,305 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.ClusterInterface; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.lang.Nullable; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.reactive.ReactiveResourceSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +public class ReactiveCouchbaseClientUtils { + + /** + * Check if the {@link ReactiveMongoDatabaseFactory} is actually bound to a + * {@link com.mongodb.reactivestreams.client.ClientSession} that has an active transaction, or if a + * {@link org.springframework.transaction.reactive.TransactionSynchronization} has been registered for the + * {@link ReactiveMongoDatabaseFactory resource} and if the associated + * {@link com.mongodb.reactivestreams.client.ClientSession} has an + * {@link com.mongodb.reactivestreams.client.ClientSession#hasActiveTransaction() active transaction}. + * + * @param databaseFactory the resource to check transactions for. Must not be {@literal null}. + * @return a {@link Mono} emitting {@literal true} if the factory has an ongoing transaction. + */ + public static Mono isTransactionActive(ReactiveCouchbaseClientFactory databaseFactory) { + + if (databaseFactory.isTransactionActive()) { + return Mono.just(true); + } + + return TransactionSynchronizationManager.forCurrentTransaction() // + .map(it -> { + + ReactiveCouchbaseResourceHolder holder = (ReactiveCouchbaseResourceHolder) it.getResource(databaseFactory); + return holder != null && holder.hasActiveTransaction(); + }) // + .onErrorResume(NoTransactionException.class, e -> Mono.just(false)); + } + + /** + * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory} using + * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getDatabase(ReactiveCouchbaseClientFactory factory) { + return doGetCouchbaseCluster(null, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION); + } + + /** + * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory}. + *
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getDatabase(ReactiveCouchbaseClientFactory factory, + SessionSynchronization sessionSynchronization) { + return doGetCouchbaseCluster(null, factory, sessionSynchronization); + } + + public static Mono getTemplate(ReactiveCouchbaseClientFactory factory, + SessionSynchronization sessionSynchronization, CouchbaseConverter converter) { + return doGetCouchbaseTemplate(null, factory, sessionSynchronization, converter); + } + + /** + * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory + * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param dbName the name of the {@link MongoDatabase} to get. + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getDatabase(String dbName, ReactiveCouchbaseClientFactory factory) { + return doGetCouchbaseCluster(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION); + } + + /** + * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory + * factory}.
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber + * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. + * + * @param dbName the name of the {@link MongoDatabase} to get. + * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. + * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. + * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. + */ + public static Mono getCluster(String dbName, ReactiveCouchbaseClientFactory factory, + SessionSynchronization sessionSynchronization) { + return doGetCouchbaseCluster(dbName, factory, sessionSynchronization); + } + + private static Mono doGetCouchbaseCluster(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization) { + + Assert.notNull(factory, "DatabaseFactory must not be null!"); + + if (sessionSynchronization == SessionSynchronization.NEVER) { + return getCouchbaseClusterOrDefault(dbName, factory); + } + + return TransactionSynchronizationManager.forCurrentTransaction() + .filter(TransactionSynchronizationManager::isSynchronizationActive) // + .flatMap(synchronizationManager -> { + + return doGetSession(synchronizationManager, factory, sessionSynchronization) // + .flatMap(it -> getCouchbaseClusterOrDefault(dbName, factory.withCore(it))); + }) // + .onErrorResume(NoTransactionException.class, e -> getCouchbaseClusterOrDefault(dbName, factory)) // hitting this + .switchIfEmpty(getCouchbaseClusterOrDefault(dbName, factory)); + } + + private static Mono doGetCouchbaseTemplate(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory, SessionSynchronization sessionSynchronization, + CouchbaseConverter converter) { + + Assert.notNull(factory, "DatabaseFactory must not be null!"); + + if (sessionSynchronization == SessionSynchronization.NEVER) { + return getCouchbaseTemplateOrDefault(dbName, factory, converter); + } + + //CouchbaseResourceHolder h = (CouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager + // .getResource(factory); + + return TransactionSynchronizationManager.forCurrentTransaction() + .filter(TransactionSynchronizationManager::isSynchronizationActive) // + .flatMap(synchronizationManager -> { + return doGetSession(synchronizationManager, factory, sessionSynchronization) // + .flatMap(it -> getCouchbaseTemplateOrDefault(dbName, factory.withCore(it), converter)); // rx TxMgr + }) // + .onErrorResume(NoTransactionException.class, + e -> { return getCouchbaseTemplateOrDefault(dbName, + getNonReactiveSession(factory) != null ? factory.withCore(getNonReactiveSession(factory)) : factory, + converter);}) // blocking TxMgr + .switchIfEmpty(getCouchbaseTemplateOrDefault(dbName, factory, converter)); + } + + private static ReactiveCouchbaseResourceHolder getNonReactiveSession(ReactiveCouchbaseClientFactory factory) { + ReactiveCouchbaseResourceHolder h = ((ReactiveCouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager + .getResource(factory.getCluster().block())); + if( h == null){ // no longer used + h = ((ReactiveCouchbaseResourceHolder) org.springframework.transaction.support.TransactionSynchronizationManager + .getResource(factory));// MN's CouchbaseTransactionManager + } + //System.err.println("getNonreactiveSession: "+ h); + return h; + } + + private static Mono getCouchbaseClusterOrDefault(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory) { + return StringUtils.hasText(dbName) ? factory.getCluster() : factory.getCluster(); + } + + private static Mono getCouchbaseTemplateOrDefault(@Nullable String dbName, + ReactiveCouchbaseClientFactory factory, CouchbaseConverter converter) { + return Mono.just(new ReactiveCouchbaseTemplate(factory, converter)); + } + + private static Mono doGetSession(TransactionSynchronizationManager synchronizationManager, + ReactiveCouchbaseClientFactory dbFactory, SessionSynchronization sessionSynchronization) { + + final ReactiveCouchbaseResourceHolder registeredHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager + .getResource(dbFactory.getCluster().block()); // make sure this wasn't saved under the wrong key!!! + + // check for native MongoDB transaction + if (registeredHolder != null + && (registeredHolder.hasCore() || registeredHolder.isSynchronizedWithTransaction())) { + System.err.println("doGetSession: got: "+registeredHolder.getCore()); + // TODO msr - mabye don't create a session unless it has an atr? + //return registeredHolder.hasCore() ? Mono.just(registeredHolder) + // : createClientSession(dbFactory).map( core -> { registeredHolder.setCore(core); return registeredHolder;}); + return Mono.just(registeredHolder); + } + + if (SessionSynchronization.ON_ACTUAL_TRANSACTION.equals(sessionSynchronization)) { + System.err.println("doGetSession: ON_ACTUAL_TRANSACTION -> empty()"); + return Mono.empty(); + } + + System.err.println("doGetSession: createClientSession()"); + + // init a non native MongoDB transaction by registering a MongoSessionSynchronization + return createClientSession(dbFactory).map(session -> { + + ReactiveCouchbaseResourceHolder newHolder = new ReactiveCouchbaseResourceHolder(session); + //newHolder.getRequiredCore().startTransaction(); + System.err.println(" need to call startTransaction() "); + + synchronizationManager + .registerSynchronization(new CouchbaseSessionSynchronization(synchronizationManager, newHolder, dbFactory)); + newHolder.setSynchronizedWithTransaction(true); + synchronizationManager.bindResource(dbFactory, newHolder); + + return newHolder; + }); + } + + private static Mono createClientSession(ReactiveCouchbaseClientFactory dbFactory) { + return null; // ?? dbFactory.getCore(TransactionOptions.transactionOptions()); + } + + /** + * MongoDB specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when + * participating in a non-native MongoDB transaction, such as a R2CBC transaction. + * + * @author Mark Paluch + * @since 2.2 + */ + private static class CouchbaseSessionSynchronization + extends ReactiveResourceSynchronization { + + private final ReactiveCouchbaseResourceHolder resourceHolder; + + CouchbaseSessionSynchronization(TransactionSynchronizationManager synchronizationManager, + ReactiveCouchbaseResourceHolder resourceHolder, ReactiveCouchbaseClientFactory dbFactory) { + + super(resourceHolder, dbFactory, synchronizationManager); + this.resourceHolder = resourceHolder; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#shouldReleaseBeforeCompletion() + */ + @Override + protected boolean shouldReleaseBeforeCompletion() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#processResourceAfterCommit(java.lang.Object) + */ + @Override + protected Mono processResourceAfterCommit(ReactiveCouchbaseResourceHolder resourceHolder) { + + if (isTransactionActive(resourceHolder)) { + return Mono.from(resourceHolder.getCore().commit()); + } + + return Mono.empty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#afterCompletion(int) + */ + @Override + public Mono afterCompletion(int status) { + + return Mono.defer(() -> { + + if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) { + + return Mono.from(resourceHolder.getCore().rollback()) // + .then(super.afterCompletion(status)); + } + + return super.afterCompletion(status); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.ReactiveResourceSynchronization#releaseResource(java.lang.Object, java.lang.Object) + */ + @Override + protected Mono releaseResource(ReactiveCouchbaseResourceHolder resourceHolder, Object resourceKey) { + + return Mono.fromRunnable(() -> { + //if (resourceHolder.hasActiveSession()) { + // resourceHolder.getRequiredSession().close(); + //} + }); + } + + private boolean isTransactionActive(ReactiveCouchbaseResourceHolder resourceHolder) { + + if (!resourceHolder.hasCore()) { + return false; + } + + return resourceHolder.getRequiredCore() != null; + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java new file mode 100644 index 000000000..4e3d09d5c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseResourceHolder.java @@ -0,0 +1,140 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.transaction; + +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; + +import java.util.HashMap; +import java.util.Map; + +/** + * MongoDB specific resource holder, wrapping a {@link CoreTransactionAttemptContext}. + * {@link ReactiveCouchbaseTransactionManager} binds instances of this class to the subscriber context. + *

+ * Note: Intended for internal usage only. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.2 + * @see ReactiveCouchbaseTransactionManager + * @see ReactiveCouchbaseTemplate + */ +// todo gp understand why this is needed +public class ReactiveCouchbaseResourceHolder extends ResourceHolderSupport { + + private @Nullable CoreTransactionAttemptContext core; // which holds the atr + Map getResultMap = new HashMap<>(); + + // private ReactiveCouchbaseClientFactory databaseFactory; + + /** + * Create a new {@link ReactiveCouchbaseResourceHolder} for a given {@link CoreTransactionAttemptContext session}. + * + * @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + public ReactiveCouchbaseResourceHolder(@Nullable CoreTransactionAttemptContext core) { + + this.core = core; + // this.databaseFactory = databaseFactory; + } + + /** + * @return the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}. + */ + @Nullable + public CoreTransactionAttemptContext getCore() { + return core; + } + + /** + * @return the required associated {@link CoreTransactionAttemptContext}. + * @throws IllegalStateException if no session is associated. + */ + CoreTransactionAttemptContext getRequiredCore() { + CoreTransactionAttemptContext core = getCore(); + if (core == null) { + throw new IllegalStateException("No CoreTransactionAttemptContext associated"); + } + return core; + } + + /* + * @return the associated {@link CouchbaseClientFactory}. + ReactiveCouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + */ + + /** + * Set the {@link CoreTransactionAttemptContext} to guard. + * + * @param core can be {@literal null}. + */ + CoreTransactionAttemptContext setCore(@Nullable CoreTransactionAttemptContext core) { + System.err.println("setCore: " + core); + return this.core = core; + } + + /** + * @return {@literal true} if session is not {@literal null}. + */ + boolean hasCore() { + return core != null; + } + + /** + * If the {@link ReactiveCouchbaseResourceHolder} is {@link #hasCore() not already associated} with a + * {@link CoreTransactionAttemptContext} the given value is {@link #setCore(CoreTransactionAttemptContext)} set} and + * returned, otherwise the current bound session is returned. + * + * @param core + * @return + */ + @Nullable + CoreTransactionAttemptContext setSessionIfAbsent(@Nullable CoreTransactionAttemptContext core) { + + if (!hasCore()) { + setCore(core); + } + + return this.core; + } + + public boolean hasActiveTransaction() { + return getCore() != null; + } + + + 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; + } + + public TransactionResultHolder transactionResultHolder(TransactionResultHolder holder, Object o) { + System.err.println("PUT: "+System.identityHashCode(o)+" "+o); + getResultMap.put(System.identityHashCode(o), holder); + return holder; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java new file mode 100644 index 000000000..80665b95f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveCouchbaseTransactionManager.java @@ -0,0 +1,499 @@ +/* + * Copyright 2019-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.java.transactions.config.TransactionOptions; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; +import org.springframework.transaction.reactive.GenericReactiveTransaction; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.Transactions; + +/** + * A {@link org.springframework.transaction.ReactiveTransactionManager} implementation that manages + * {@link CoreTransactionAttemptContext} based transactions for a single {@link CouchbaseClientFactory}. + *

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

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

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

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

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

+ * Optionally it is possible to set default {@link TransactionQueryOptions transaction options} defining {link + * com.xxxxxxx.ReadConcern} and {link com.xxxxxxx.WriteConcern}. + * + * @see #setDatabaseFactory(ReactiveCouchbaseClientFactory) + */ + public ReactiveCouchbaseTransactionManager() {} + + /** + * Create a new {@link ReactiveCouchbaseTransactionManager} obtaining sessions from the given + * {@link CouchbaseClientFactory} applying the given {@link TransactionQueryOptions options}, if present, when + * starting a new transaction. + * + * @param databaseFactory must not be {@literal null}. + */ + public ReactiveCouchbaseTransactionManager(ReactiveCouchbaseClientFactory databaseFactory) { + Assert.notNull(databaseFactory, "DatabaseFactory must not be null!"); + this.databaseFactory = databaseFactory; // should be a clone? TransactionSynchronizationManager binds objs to it + 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) + */ + @Override + protected Object doGetTransaction(TransactionSynchronizationManager synchronizationManager) + throws TransactionException { + // creation of a new ReactiveCouchbaseTransactionObject (i.e. transaction). + // with an attempt to get the resourceHolder from the synchronizationManager + ReactiveCouchbaseResourceHolder resourceHolder = (ReactiveCouchbaseResourceHolder) synchronizationManager + .getResource(getRequiredDatabaseFactory().getCluster().block()); + // TODO ACR from couchbase + // resourceHolder.getSession().setAttemptContextReactive(null); + return new ReactiveCouchbaseTransactionObject(resourceHolder); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(java.lang.Object) + */ + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return extractCouchbaseTransaction(transaction).hasResourceHolder(); + } + + /** + * doBegin() attaches the atr from the transactionOperator in the transactionDefinition to the transaction (via + * resourceHolder -> Clientsession) (non-Javadoc) + * + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doBegin(org.springframework.transaction.reactive.TransactionSynchronizationManager, + * java.lang.Object, org.springframework.transaction.TransactionDefinition) + */ + @Override + protected Mono doBegin(TransactionSynchronizationManager synchronizationManager, Object transaction, + TransactionDefinition definition) throws TransactionException { + + return Mono.defer(() -> { + + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(transaction); + // TODO mr - why aren't we creating the AttemptContext here in the client session in the resourceholder? + Mono holder = newResourceHolder(definition, + TransactionOptions.transactionOptions()); + return holder.doOnNext(resourceHolder -> { + couchbaseTransactionObject.setResourceHolder(resourceHolder); + if (logger.isDebugEnabled()) { + logger.debug( + String.format("About to start transaction for session %s.", debugString(resourceHolder.getCore()))); + } + }).doOnNext(resourceHolder -> { + couchbaseTransactionObject.startTransaction(); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getCore()))); + } + })// + .onErrorMap(ex -> new TransactionSystemException( + String.format("Could not start Couchbase transaction for session %s.", + debugString(couchbaseTransactionObject.getCore())), + ex)) + .doOnSuccess(resourceHolder -> { + System.err.println("ReactiveCouchbaseTransactionManager: " + this); + System.err.println( + "bindResource: " + getRequiredDatabaseFactory().getCluster().block() + " value: " + resourceHolder); + synchronizationManager.bindResource(getRequiredDatabaseFactory().getCluster().block(), resourceHolder); + }).then(); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSuspend(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object) + */ + @Override + protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) + throws TransactionException { + + return Mono.fromSupplier(() -> { + + ReactiveCouchbaseTransactionObject mongoTransactionObject = extractCouchbaseTransaction(transaction); + mongoTransactionObject.setResourceHolder(null); + + return synchronizationManager.unbindResource(getRequiredDatabaseFactory()); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doResume(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object, java.lang.Object) + */ + @Override + protected Mono doResume(TransactionSynchronizationManager synchronizationManager, @Nullable Object transaction, + Object suspendedResources) { + return Mono + .fromRunnable(() -> synchronizationManager.bindResource(getRequiredDatabaseFactory(), suspendedResources)); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCommit(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected final Mono doCommit(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + return Mono.defer(() -> { + + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to doCommit transaction for session %s.", + debugString(couchbaseTransactionObject.getCore()))); + } + + return doCommit(synchronizationManager, couchbaseTransactionObject).onErrorMap(ex -> { + return new TransactionSystemException(String.format("Could not commit Couchbase transaction for session %s.", + debugString(couchbaseTransactionObject.getCore())), ex); + }); + }); + } + + /** + * Customization hook to perform an actual commit of the given transaction.
+ * If a commit operation encounters an error, the XxxxxxXX driver throws a {@link CouchbaseException} holding + * {@literal error labels}.
+ * By default those labels are ignored, nevertheless one might check for {@link CouchbaseException transient commit + * errors labels} and retry the the commit. + * + * @param synchronizationManager reactive synchronization manager. + * @param transactionObject never {@literal null}. + */ + protected Mono doCommit(TransactionSynchronizationManager synchronizationManager, + ReactiveCouchbaseTransactionObject transactionObject) { + return transactionObject.commitTransaction(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doRollback(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected Mono doRollback(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) { + + return Mono.defer(() -> { + + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = extractCouchbaseTransaction(status); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to abort transaction for session %s.", + debugString(couchbaseTransactionObject.getCore()))); + } + + return couchbaseTransactionObject.abortTransaction().onErrorResume(CouchbaseException.class, ex -> { + return Mono.error(new TransactionSystemException(String.format("Could not abort transaction for session %s.", + debugString(couchbaseTransactionObject.getCore())), ex)); + }); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + + return Mono.fromRunnable(() -> { + ReactiveCouchbaseTransactionObject transactionObject = extractCouchbaseTransaction(status); + transactionObject.getRequiredResourceHolder().setRollbackOnly(); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCleanupAfterCompletion(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object) + */ + @Override + protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager, + Object transaction) { + + Assert.isInstanceOf(ReactiveCouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", + ReactiveCouchbaseTransactionObject.class, transaction.getClass())); + + return Mono.fromRunnable(() -> { + ReactiveCouchbaseTransactionObject couchbaseTransactionObject = (ReactiveCouchbaseTransactionObject) transaction; + + // Remove the connection holder from the thread. + synchronizationManager.unbindResource(getRequiredDatabaseFactory().getCluster().block()); + couchbaseTransactionObject.getRequiredResourceHolder().clear(); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("About to release Session %s after transaction.", + debugString(couchbaseTransactionObject.getCore()))); + } + + couchbaseTransactionObject.closeSession(); + }); + } + + /** + * Set the {@link CouchbaseClientFactory} that this instance should manage transactions for. + * + * @param databaseFactory must not be {@literal null}. + */ + public void setDatabaseFactory(ReactiveCouchbaseClientFactory databaseFactory) { + + Assert.notNull(databaseFactory, "DatabaseFactory must not be null!"); + this.databaseFactory = databaseFactory; + } + + /** + * Get the {@link CouchbaseClientFactory} that this instance manages transactions for. + * + * @return can be {@literal null}. + */ + @Nullable + public ReactiveCouchbaseClientFactory getDatabaseFactory() { + return databaseFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + getRequiredDatabaseFactory(); + } + + private Mono newResourceHolder(TransactionDefinition definition, + TransactionOptions options) { + + ReactiveCouchbaseClientFactory dbFactory = getRequiredDatabaseFactory(); + // TODO MSR : config should be derived from config that was used for `transactions` + Mono sess = Mono.just(dbFactory.getTransactionResources(options, null)); + return sess; + } + + /** + * @throws IllegalStateException if {@link #databaseFactory} is {@literal null}. + */ + private ReactiveCouchbaseClientFactory getRequiredDatabaseFactory() { + Assert.state(databaseFactory != null, + "ReactiveCouchbaseTransactionManager operates upon a CouchbaseClientFactory. Did you forget to provide one? It's required."); + return databaseFactory; + } + + private static ReactiveCouchbaseTransactionObject extractCouchbaseTransaction(Object transaction) { + + Assert.isInstanceOf(ReactiveCouchbaseTransactionObject.class, transaction, + () -> String.format("Expected to find a %s but it turned out to be %s.", + ReactiveCouchbaseTransactionObject.class, transaction.getClass())); + + return (ReactiveCouchbaseTransactionObject) transaction; + } + + private static ReactiveCouchbaseTransactionObject extractCouchbaseTransaction(GenericReactiveTransaction status) { + + Assert.isInstanceOf(ReactiveCouchbaseTransactionObject.class, status.getTransaction(), + () -> String.format("Expected to find a %s but it turned out to be %s.", + ReactiveCouchbaseTransactionObject.class, status.getTransaction().getClass())); + + return (ReactiveCouchbaseTransactionObject) status.getTransaction(); + } + + private static String debugString(@Nullable CoreTransactionAttemptContext session) { + + if (session == null) { + return "null"; + } + + String debugString = String.format("[%s@%s ", ClassUtils.getShortName(session.getClass()), + Integer.toHexString(session.hashCode())); + + try { + debugString += String.format("core=%s", session); + } catch (RuntimeException e) { + debugString += String.format("error = %s", e.getMessage()); + } + + debugString += "]"; + + return debugString; + } + + /** + * Couchbase specific transaction object, representing a {@link ReactiveCouchbaseResourceHolder}. Used as transaction + * object by {@link ReactiveCouchbaseTransactionManager}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.2 + * @see ReactiveCouchbaseResourceHolder + */ + protected static class ReactiveCouchbaseTransactionObject implements SmartTransactionObject { + + public @Nullable ReactiveCouchbaseResourceHolder resourceHolder; + + ReactiveCouchbaseTransactionObject(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * Set the {@link ReactiveCouchbaseResourceHolder}. + * + * @param resourceHolder can be {@literal null}. + */ + void setResourceHolder(@Nullable ReactiveCouchbaseResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + /** + * @return {@literal true} if a {@link ReactiveCouchbaseResourceHolder} is set. + */ + final boolean hasResourceHolder() { + return resourceHolder != null; + } + + /** + * Start a XxxxxxXX transaction optionally given {@link TransactionQueryOptions}. todo gp how to expose + * TransactionOptions + * + * @param options can be {@literal null} + */ + void startTransaction() { + + CoreTransactionAttemptContext core = getRequiredCore(); + // core.startTransaction(); + } + + /** + * Commit the transaction. + */ + public Mono commitTransaction() { + return getRequiredCore().commit(); + } + + /** + * Rollback (abort) the transaction. + */ + public Mono abortTransaction() { + return getRequiredCore().rollback(); + } + + /** + * Close a {@link CoreTransactionAttemptContext} without regard to its transactional state. + */ + void closeSession() { + CoreTransactionAttemptContext session = getRequiredCore(); + // if (session.getServerSession() != null && !session.getServerSession().isClosed()) { + // session.close(); + // } + } + + @Nullable + public CoreTransactionAttemptContext getCore() { + return resourceHolder != null ? resourceHolder.getCore() : null; + } + + private ReactiveCouchbaseResourceHolder getRequiredResourceHolder() { + + Assert.state(resourceHolder != null, "ReactiveMongoResourceHolder is required but not present. o_O"); + return resourceHolder; + } + + private CoreTransactionAttemptContext getRequiredCore() { + CoreTransactionAttemptContext core = getCore(); + Assert.state(core != null, "A CoreTransactionAttemptContext is required but it turned out to be null."); + return core; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#isRollbackOnly() + */ + @Override + public boolean isRollbackOnly() { + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.SmartTransactionObject#flush() + */ + @Override + public void flush() { + throw new UnsupportedOperationException("flush() not supported"); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ReactiveTransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveTransactionsWrapper.java new file mode 100644 index 000000000..b4aa21ce7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ReactiveTransactionsWrapper.java @@ -0,0 +1,71 @@ +package org.springframework.data.couchbase.transaction; + +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.reactive.TransactionContextManager; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; + +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; + +// todo gp needed now Transactions has gone? +public class ReactiveTransactionsWrapper /* wraps ReactiveTransactions */ { + ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; + + public ReactiveTransactionsWrapper(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory) { + this.reactiveCouchbaseClientFactory = reactiveCouchbaseClientFactory; + } + + /** + * A convenience wrapper around {@link TransactionsReactive#run}, that provides a default + * PerTransactionConfig. + */ + + public Mono run(Function> transactionLogic) { + return run(transactionLogic, null); + } + + public Mono run(Function> transactionLogic, + TransactionOptions perConfig) { + // todo gp this is duplicating a lot of logic from the core loop, and is hopefully not needed. + // todo ^^^ I think I removed all the duplicate logic. + Function> newTransactionLogic = (ctx) -> { + ReactiveCouchbaseResourceHolder resourceHolder = reactiveCouchbaseClientFactory.getTransactionResources( + TransactionOptions.transactionOptions(), AttemptContextReactiveAccessor.getCore(ctx)); + Mono sync = TransactionContextManager.currentContext() + .map(TransactionSynchronizationManager::new).flatMap(synchronizationManager -> { + synchronizationManager.bindResource(reactiveCouchbaseClientFactory.getBlockingCluster(), resourceHolder); + prepareSynchronization(synchronizationManager, null, new CouchbaseTransactionDefinition()); + return transactionLogic.apply(ctx) // <---- execute the transaction + .thenReturn(ctx).then(Mono.just(synchronizationManager)); + }); + return sync.contextWrite(TransactionContextManager.getOrCreateContext()) + .contextWrite(TransactionContextManager.getOrCreateContextHolder()); + }; + + return reactiveCouchbaseClientFactory.getBlockingCluster().reactive().transactions().run(newTransactionLogic, + perConfig); + + } + + private static void prepareSynchronization(TransactionSynchronizationManager synchronizationManager, + ReactiveTransaction status, TransactionDefinition definition) { + // if (status.isNewTransaction()) { + synchronizationManager.setActualTransactionActive(false /*status.hasTransaction()*/); + synchronizationManager.setCurrentTransactionIsolationLevel( + definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() + : null); + synchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); + synchronizationManager.setCurrentTransactionName(definition.getName()); + synchronizationManager.initSynchronization(); + // } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/ServerSession.java b/src/main/java/org/springframework/data/couchbase/transaction/ServerSession.java new file mode 100644 index 000000000..d53cf1f0a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/ServerSession.java @@ -0,0 +1,19 @@ +package org.springframework.data.couchbase.transaction; + +/** + * used only by ClientSession.getServerSession() - which returns null + */ + +public interface ServerSession { + String getIdentifier(); + + long getTransactionNumber(); + + long advanceTransactionNumber(); + + boolean isClosed(); + + void markDirty(); + + boolean isMarkedDirty(); +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java new file mode 100644 index 000000000..ebf1c284b --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/SessionAwareMethodInterceptor.java @@ -0,0 +1,213 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.transaction; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Optional; +import java.util.function.BiFunction; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodClassKey; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; + +/** + * {@link MethodInterceptor} implementation looking up and invoking an alternative target method having + * {@link CoreTransactionAttemptContext} as its first argument. This allows seamless integration with the existing code base. + *
+ * The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself + * like (eg. TODO) and decorate them + * if not already proxied. + * + * @param Type of the actual Mongo Database. + * @param Type of the actual Mongo Collection. + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ +public class SessionAwareMethodInterceptor implements MethodInterceptor { + + private static final MethodCache METHOD_CACHE = new MethodCache(); + + private final ReactiveCouchbaseResourceHolder session; + private final ReactiveCouchbaseResourceHolderOperator collectionDecorator; + private final ReactiveCouchbaseResourceHolderOperator databaseDecorator; + private final Object target; + private final Class targetType; + private final Class collectionType; + private final Class databaseType; + private final Class sessionType; + + /** + * Create a new SessionAwareMethodInterceptor for given target. + * + * @param session the {@link CoreTransactionAttemptContext} to be used on invocation. + * @param target the original target object. + * @param databaseType the MongoDB database type + * @param databaseDecorator a {@link ReactiveCouchbaseResourceHolderOperator} used to create the proxy for an imperative / reactive + * {@code MongoDatabase}. + * @param collectionType the MongoDB collection type. + * @param collectionDecorator a {@link ReactiveCouchbaseResourceHolderOperator} used to create the proxy for an imperative / reactive + * {@code MongoCollection}. + * @param target object type. + */ + public SessionAwareMethodInterceptor(ReactiveCouchbaseResourceHolder session, T target, Class sessionType, + Class databaseType, ReactiveCouchbaseResourceHolderOperator databaseDecorator, Class collectionType, + ReactiveCouchbaseResourceHolderOperator collectionDecorator) { + + Assert.notNull(session, "CoreTransactionAttemptContext must not be null!"); + Assert.notNull(target, "Target must not be null!"); + Assert.notNull(sessionType, "SessionType must not be null!"); + Assert.notNull(databaseType, "Database type must not be null!"); + Assert.notNull(databaseDecorator, "Database CoreTransactionAttemptContextOperator must not be null!"); + Assert.notNull(collectionType, "Collection type must not be null!"); + Assert.notNull(collectionDecorator, "Collection CoreTransactionAttemptContextOperator must not be null!"); + + this.session = session; + this.target = target; + this.databaseType = ClassUtils.getUserClass(databaseType); + this.collectionType = ClassUtils.getUserClass(collectionType); + this.collectionDecorator = collectionDecorator; + this.databaseDecorator = databaseDecorator; + + this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType; + this.sessionType = sessionType; + } + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor(org.aopalliance.intercept.MethodInvocation) + */ + @Nullable + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + + if (requiresDecoration(methodInvocation.getMethod())) { + + Object target = methodInvocation.proceed(); + if (target instanceof Proxy) { + return target; + } + + return decorate(target); + } + + if (!requiresSession(methodInvocation.getMethod())) { + return methodInvocation.proceed(); + } + + Optional targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType, sessionType); + + return !targetMethod.isPresent() ? methodInvocation.proceed() + : ReflectionUtils.invokeMethod(targetMethod.get(), target, + prependSessionToArguments(session, methodInvocation)); + } + + private boolean requiresDecoration(Method method) { + + return ClassUtils.isAssignable(databaseType, method.getReturnType()) + || ClassUtils.isAssignable(collectionType, method.getReturnType()); + } + + @SuppressWarnings("unchecked") + protected Object decorate(Object target) { + + return ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseDecorator.apply(session, target) + : collectionDecorator.apply(session, target); + } + + private static boolean requiresSession(Method method) { + + if (method.getParameterCount() == 0 + || !ClassUtils.isAssignable(CoreTransactionAttemptContext.class, method.getParameterTypes()[0])) { + return true; + } + + return false; + } + + private static Object[] prependSessionToArguments(ReactiveCouchbaseResourceHolder session, MethodInvocation invocation) { + + Object[] args = new Object[invocation.getArguments().length + 1]; + + args[0] = session; + System.arraycopy(invocation.getArguments(), 0, args, 1, invocation.getArguments().length); + + return args; + } + + /** + * Simple {@link Method} to {@link Method} caching facility for {@link CoreTransactionAttemptContext} overloaded targets. + * + * @since 2.1 + * @author Christoph Strobl + */ + static class MethodCache { + + private final ConcurrentReferenceHashMap> cache = new ConcurrentReferenceHashMap<>(); + + /** + * Lookup the target {@link Method}. + * + * @param method + * @param targetClass + * @return + */ + Optional lookup(Method method, Class targetClass, Class sessionType) { + + return cache.computeIfAbsent(new MethodClassKey(method, targetClass), + val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType))); + } + + @Nullable + private Method findTargetWithSession(Method sourceMethod, Class targetType, + Class sessionType) { + + Class[] argTypes = sourceMethod.getParameterTypes(); + Class[] args = new Class[argTypes.length + 1]; + args[0] = sessionType; + System.arraycopy(argTypes, 0, args, 1, argTypes.length); + + return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args); + } + + /** + * Check whether the cache contains an entry for {@link Method} and {@link Class}. + * + * @param method + * @param targetClass + * @return + */ + boolean contains(Method method, Class targetClass) { + return cache.containsKey(new MethodClassKey(method, targetClass)); + } + } + + /** + * Represents an operation upon two operands of the same type, producing a result of the same type as the operands + * accepting {@link CoreTransactionAttemptContext}. This is a specialization of {@link BiFunction} for the case where the operands and + * the result are all of the same type. + * + * @param the type of the operands and result of the operator + */ + public interface ReactiveCouchbaseResourceHolderOperator extends BiFunction {} +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/SessionSynchronization.java b/src/main/java/org/springframework/data/couchbase/transaction/SessionSynchronization.java new file mode 100644 index 000000000..d62633ba9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/SessionSynchronization.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.transaction; + +/** + * TODO MSR not used + * {@link SessionSynchronization} is used along with {@link org.springframework.data.couchbase.core.CouchbaseTemplate} to + * define in which type of transactions to participate if any. + * + * @author Michael Reiche + */ +public enum SessionSynchronization { + + /** + * Synchronize with any transaction even with empty transactions and initiate a MongoDB transaction when doing so by + * registering a MongoDB specific {@link org.springframework.transaction.support.ResourceHolderSynchronization}. + */ + ALWAYS, + + /** + * Synchronize with native MongoDB transactions initiated via {@link ReactiveCouchbaseTransactionManager}. + */ + ON_ACTUAL_TRANSACTION, + NEVER; + +} diff --git a/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java new file mode 100644 index 000000000..7f3f9c104 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/TransactionsWrapper.java @@ -0,0 +1,157 @@ +package org.springframework.data.couchbase.transaction; + +import com.couchbase.client.core.error.transaction.TransactionOperationFailedException; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.ReactiveTransactionAttemptContext; +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import org.springframework.data.couchbase.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 { + 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/interceptor/CouchbaseTransactionInterceptor.java b/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java new file mode 100644 index 000000000..66b1c00e2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/transaction/interceptor/CouchbaseTransactionInterceptor.java @@ -0,0 +1,418 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.transaction.interceptor; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Properties; +import java.util.concurrent.ConcurrentMap; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.lang.Nullable; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.interceptor.DefaultTransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.interceptor.TransactionProxyFactoryBean; +import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * AOP Alliance MethodInterceptor for declarative transaction + * management using the common Spring transaction infrastructure + * ({@link org.springframework.transaction.PlatformTransactionManager}/ + * {@link org.springframework.transaction.ReactiveTransactionManager}). + * + *

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

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

Transaction manager and transaction attributes still need to be set. + * @see #setTransactionManager + * @see #setTransactionAttributes(java.util.Properties) + * @see #setTransactionAttributeSource(TransactionAttributeSource) + */ + public CouchbaseTransactionInterceptor() { + if (reactiveStreamsPresent) { + this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); + } + else { + this.reactiveAdapterRegistry = null; + } + } + + /** + * Create a new TransactionInterceptor. + * @param ptm the default transaction manager to perform the actual transaction management + * @param tas the attribute source to be used to find transaction attributes + * @since 5.2.5 + * @see #setTransactionManager + * @see #setTransactionAttributeSource + */ + public CouchbaseTransactionInterceptor(TransactionManager ptm, TransactionAttributeSource tas) { + this(); + setTransactionManager(ptm); + setTransactionAttributeSource(tas); + } + + /** + * Create a new TransactionInterceptor. + * @param ptm the default transaction manager to perform the actual transaction management + * @param tas the attribute source to be used to find transaction attributes + * @see #setTransactionManager + * @see #setTransactionAttributeSource + * @deprecated as of 5.2.5, in favor of + * {@link #CouchbaseTransactionInterceptor(TransactionManager, TransactionAttributeSource)} + */ + @Deprecated + public CouchbaseTransactionInterceptor(PlatformTransactionManager ptm, TransactionAttributeSource tas) { + this(); + setTransactionManager(ptm); + setTransactionAttributeSource(tas); + } + + /** + * Create a new TransactionInterceptor. + * @param ptm the default transaction manager to perform the actual transaction management + * @param attributes the transaction attributes in properties format + * @see #setTransactionManager + * @see #setTransactionAttributes(java.util.Properties) + * @deprecated as of 5.2.5, in favor of {@link #setTransactionAttributes(Properties)} + */ + @Deprecated + public CouchbaseTransactionInterceptor(PlatformTransactionManager ptm, Properties attributes) { + this(); + setTransactionManager(ptm); + setTransactionAttributes(attributes); + } + + + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + // Work out the target class: may be {@code null}. + // The TransactionAttributeSource should be passed the target class + // as well as the method, which may be from an interface. + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + + // Adapt to TransactionAspectSupport's invokeWithinTransaction... + return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() { + @Override + @Nullable + public Object proceedWithInvocation() throws Throwable { + return invocation.proceed(); + } + @Override + public Object getTarget() { + return invocation.getThis(); + } + @Override + public Object[] getArguments() { + return invocation.getArguments(); + } + }); + } + + /** + * General delegate for around-advice-based subclasses, delegating to several other template + * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager} + * as well as regular {@link PlatformTransactionManager} implementations and + * {@link ReactiveTransactionManager} implementations for reactive return types. + * @param method the Method being invoked + * @param targetClass the target class that we're invoking the method on + * @param invocation the callback to use for proceeding with the target invocation + * @return the return value of the method, if any + * @throws Throwable propagated from the target invocation + */ + @Nullable + protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + + // If the transaction attribute is null, the method is non-transactional. + TransactionAttributeSource tas = getTransactionAttributeSource(); + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + final TransactionManager tm = determineTransactionManager(txAttr); + + if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) { + return super.invokeWithinTransaction(method, targetClass, invocation); + } + + PlatformTransactionManager ptm = asPlatformTransactionManager(tm); + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { + //return super.invokeWithinTransaction(method, targetClass, invocation); + // Standard transaction demarcation with getTransaction and commit/rollback calls. + + Object retVal = null; + boolean success=false; + do { + TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); + try { + // This is an around advice: Invoke the next interceptor in the chain. + // This will normally result in a target object being invoked. + retVal = invocation.proceedWithInvocation(); + success = true; + } catch (Throwable ex) { + // target invocation exception + completeTransactionAfterThrowing(txInfo, + ex); + //throw ex; + } finally { + cleanupTransactionInfo(txInfo); + } + if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { + // Set rollback-only in case of Vavr failure matching our rollback rules... + TransactionStatus status = txInfo.getTransactionStatus(); + if (status != null && txAttr != null) { + retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); + } + } + + if( retVal != null) { + // this could go directly after succeed = true except for the cleanupTransactionInfo(txInfo) and the vavrPresent + commitTransactionAfterReturning(txInfo); + } + } while (!success); + + return retVal; + } + + else { + + Object result; + final ThrowableHolder throwableHolder = new ThrowableHolder(); + + // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in. + try { + result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> { + TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status); + try { + Object retVal = invocation.proceedWithInvocation(); + if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { + // Set rollback-only in case of Vavr failure matching our rollback rules... + retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); + } + return retVal; + } + catch (Throwable ex) { + if (txAttr.rollbackOn(ex)) { + // A RuntimeException: will lead to a rollback. + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + else { + throw new ThrowableHolderException(ex); + } + } + else { + // A normal return value: will lead to a commit. + throwableHolder.throwable = ex; + return null; + } + } + finally { + cleanupTransactionInfo(txInfo); + } + }); + } + catch (ThrowableHolderException ex) { + throw ex.getCause(); + } + catch (TransactionSystemException ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + ex2.initApplicationException(throwableHolder.throwable); + } + throw ex2; + } + catch (Throwable ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + } + throw ex2; + } + + // Check result state: It might indicate a Throwable to rethrow. + if (throwableHolder.throwable != null) { + throw throwableHolder.throwable; + } + return result; + } + } + + // from TransactionAspectSupport + @Nullable + private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) { + if (transactionManager == null || transactionManager instanceof PlatformTransactionManager) { + return (PlatformTransactionManager) transactionManager; + } + else { + throw new IllegalStateException( + "Specified transaction manager is not a PlatformTransactionManager: " + transactionManager); + } + } + + // from TransactionAspectSupport + private String methodIdentification(Method method, @Nullable Class targetClass, + @Nullable TransactionAttribute txAttr) { + + String methodIdentification = methodIdentification(method, targetClass); + if (methodIdentification == null) { + if (txAttr instanceof DefaultTransactionAttribute) { + methodIdentification = ((DefaultTransactionAttribute) txAttr).getDescriptor(); + } + if (methodIdentification == null) { + methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); + } + } + return methodIdentification; + } + + // from TransactionAspectSupport + /** + * Internal holder class for a Throwable, used as a RuntimeException to be + * thrown from a TransactionCallback (and subsequently unwrapped again). + */ + @SuppressWarnings("serial") + private static class ThrowableHolderException extends RuntimeException { + + public ThrowableHolderException(Throwable throwable) { + super(throwable); + } + + @Override + public String toString() { + return getCause().toString(); + } + } + + // from TransactionAspectSupport + /** + * Internal holder class for a Throwable in a callback transaction model. + */ + private static class ThrowableHolder { + + @Nullable + public Throwable throwable; + } + + // From TransactionAspectSupport + /** + * Inner class to avoid a hard dependency on the Vavr library at runtime. + */ + private static class VavrDelegate { + + public static boolean isVavrTry(Object retVal) { + return false; // (retVal instanceof Try); + } + + public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAttr, TransactionStatus status) { + throw new RuntimeException("no Vavr support"); + /* + return ((Try) retVal).onFailure(ex -> { + if (txAttr.rollbackOn(ex)) { + status.setRollbackOnly(); + } + }); + */ + } + } + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void writeObject(ObjectOutputStream oos) throws IOException { + // Rely on default serialization, although this class itself doesn't carry state anyway... + oos.defaultWriteObject(); + + // Deserialize superclass fields. + oos.writeObject(getTransactionManagerBeanName()); + oos.writeObject(getTransactionManager()); + oos.writeObject(getTransactionAttributeSource()); + oos.writeObject(getBeanFactory()); + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, although this class itself doesn't carry state anyway... + ois.defaultReadObject(); + + // Serialize all relevant superclass fields. + // Superclass can't implement Serializable because it also serves as base class + // for AspectJ aspects (which are not allowed to implement Serializable)! + setTransactionManagerBeanName((String) ois.readObject()); + setTransactionManager((PlatformTransactionManager) ois.readObject()); + setTransactionAttributeSource((TransactionAttributeSource) ois.readObject()); + setBeanFactory((BeanFactory) ois.readObject()); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java index 65aea1e39..a82c532ff 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java @@ -19,10 +19,13 @@ import com.couchbase.client.java.query.QueryOptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.util.UUID; diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index 1500fa810..648ef2a1f 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -28,14 +28,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.query.QueryOptions; @@ -45,6 +44,7 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseCacheIntegrationTests extends JavaIntegrationTests { volatile CouchbaseCache cache; @@ -58,9 +58,9 @@ public void beforeEach() { cache = CouchbaseCacheManager.create(couchbaseTemplate.getCouchbaseClientFactory()).createCouchbaseCache("myCache", CouchbaseCacheConfiguration.defaultCacheConfig()); clear(cache); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - cacheManager = ac.getBean(CouchbaseCacheManager.class); - userRepository = ac.getBean(UserRepository.class); + // ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // cacheManager = ac.getBean(CouchbaseCacheManager.class); + // userRepository = ac.getBean(UserRepository.class); } @AfterEach diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index 2e9f14d1a..1adcd7ff9 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -35,8 +35,10 @@ import java.util.Set; import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; @@ -48,6 +50,7 @@ import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.PersonValue; import org.springframework.data.couchbase.domain.Submission; @@ -56,6 +59,7 @@ import org.springframework.data.couchbase.domain.UserAnnotated2; import org.springframework.data.couchbase.domain.UserAnnotated3; import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.transactions.CouchbaseReactiveTransactionNativeTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; @@ -65,6 +69,7 @@ import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; ; @@ -75,12 +80,15 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { - super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated2.class).all(); diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index dcd13b5ab..d8ffd4e58 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -30,18 +31,19 @@ import java.util.UUID; import java.util.stream.Collectors; -import com.couchbase.client.core.msg.kv.DurabilityLevel; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -55,10 +57,12 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.kv.ExistsOptions; import com.couchbase.client.java.kv.GetAnyReplicaOptions; @@ -68,7 +72,6 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryScanConsistency; /** * Query tests Theses tests rely on a cb server running This class tests collection support with @@ -78,8 +81,12 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + Airport vie = new Airport("airports::vie", "vie", "loww"); @BeforeAll @@ -104,18 +111,16 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); } @AfterEach @@ -124,8 +129,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); // then call the super method super.afterEach(); } @@ -138,8 +142,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -181,8 +185,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -206,8 +210,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(daveUsers).all(); + .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -223,17 +227,17 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all().collectList().block(); + .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); - couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); - couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(UserSubmission.class).withConsistency(REQUEST_PLUS).all(); } @@ -248,8 +252,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -273,8 +277,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) + .withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -297,18 +301,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -316,8 +320,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count2); } finally { @@ -341,26 +345,29 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count() - .block(); + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); - // count (distinct { iata, icao } ) - Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block(); + // count( distinct (all fields in icaoClass) // which only has one field + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Class icaoClass = (new Object() { + String icao; + }).getClass(); + long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(icaoClass).withConsistency(REQUEST_PLUS).count().block(); assertEquals(7, count2); } finally { @@ -433,9 +440,8 @@ public void findByQuery() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("441")); try { - List found = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()); @@ -486,9 +492,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("495")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName) + .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -577,9 +583,8 @@ public void findByQueryOther() { // 4 Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("594")); try { - List found = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).all(); + List found = couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), found.get(0).getId()); } finally { couchbaseTemplate.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()); @@ -630,9 +635,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("648")); - List removeResults = couchbaseTemplate.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all(); + List removeResults = couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -695,9 +700,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options).all()); + assertThrows(AmbiguousTimeoutException.class, () -> couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all()); } @Test @@ -735,9 +739,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all()); + () -> couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) + .withOptions(options).all()); } @Test @@ -761,9 +765,8 @@ public void testScopeCollectionAnnotation() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) + .matching(query).withConsistency(REQUEST_PLUS).all(); assertEquals(saved, found.get(0), "should have found what was saved"); List notfound = couchbaseTemplate.findByQuery(UserCol.class).inScope(CollectionIdentifier.DEFAULT_SCOPE) .inCollection(CollectionIdentifier.DEFAULT_COLLECTION).matching(query).all(); @@ -785,9 +788,8 @@ public void testScopeCollectionRepoWith() { try { UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) .one(user); - List found = couchbaseTemplate.findByQuery(UserCol.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .matching(query).all(); + List found = couchbaseTemplate.findByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName) + .matching(query).withConsistency(REQUEST_PLUS).all(); assertEquals(saved, found.get(0), "should have found what was saved"); List notfound = couchbaseTemplate.findByQuery(UserCol.class).inScope(CollectionIdentifier.DEFAULT_SCOPE) .inCollection(CollectionIdentifier.DEFAULT_COLLECTION).matching(query).all(); @@ -808,30 +810,30 @@ void testFluentApi() { DurabilityLevel dl = DurabilityLevel.NONE; User result; RemoveResult rr; - result = couchbaseTemplate.insertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) - .one(user1); - assertEquals(user1,result); - result = couchbaseTemplate.upsertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + result = couchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); - assertEquals(user1,result); - result = couchbaseTemplate.replaceById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + assertEquals(user1, result); + result = couchbaseTemplate.upsertById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1); - assertEquals(user1,result); - rr = couchbaseTemplate.removeById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) + assertEquals(user1, result); + result = couchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1); + assertEquals(user1, result); + rr = couchbaseTemplate.removeById(User.class).inScope(scopeName).inCollection(collectionName).withDurability(dl) .one(user1.getId()); assertEquals(rr.getId(), user1.getId()); - assertEquals(user1,result); - result = reactiveCouchbaseTemplate.insertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) - .one(user1).block(); - assertEquals(user1,result); - result = reactiveCouchbaseTemplate.upsertById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) - .one(user1).block(); - assertEquals(user1,result); - result = reactiveCouchbaseTemplate.replaceById(User.class).withDurability(dl).inScope(scopeName) - .inCollection(collectionName).one(user1).block(); - assertEquals(user1,result); - rr = reactiveCouchbaseTemplate.removeById(User.class).withDurability(dl).inScope(scopeName).inCollection(collectionName) - .one(user1.getId()).block(); + assertEquals(user1, result); + result = reactiveCouchbaseTemplate.insertById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1).block(); + assertEquals(user1, result); + result = reactiveCouchbaseTemplate.upsertById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1).block(); + assertEquals(user1, result); + result = reactiveCouchbaseTemplate.replaceById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1).block(); + assertEquals(user1, result); + rr = reactiveCouchbaseTemplate.removeById(User.class).inScope(scopeName).inCollection(collectionName) + .withDurability(dl).one(user1.getId()).block(); assertEquals(rr.getId(), user1.getId()); } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index e6e749f12..691f0dc8a 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -33,11 +33,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AssessmentDO; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -53,6 +55,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * Query tests Theses tests rely on a cb server running @@ -63,8 +66,13 @@ * @author Mauro Monti */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests { + @Autowired + public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { @@ -129,8 +137,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where(i("firstname")).like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) - .matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).matching(specialUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -144,7 +152,7 @@ void findAssessmentDO() { Query specialUsers = new Query(QueryCriteria.where(i("id")).is(ado.getId())); final List foundUsers = couchbaseTemplate.findByQuery(AssessmentDO.class) - .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals("123", foundUsers.get(0).getId(), "id"); assertEquals("44444444", foundUsers.get(0).getDocumentId(), "documentId"); assertEquals(ado, foundUsers.get(0)); @@ -172,7 +180,7 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).matching(daveUsers).all(); + .as(UserSubmissionProjected.class).matching(daveUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -189,11 +197,11 @@ void findByMatchingQueryProjected() { Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(REQUEST_PLUS).matching(specialUsers).all(); + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(REQUEST_PLUS).matching(specialUsers).all().collectList().block(); + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(1, foundUsersReactive.size()); couchbaseTemplate.removeById(User.class).all(Arrays.asList(user1.getId(), user2.getId(), specialUser.getId())); @@ -230,7 +238,7 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where(i("firstname")).notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).matching(nonSpecialUsers).withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).one(user2.getId())); @@ -267,6 +275,15 @@ void distinct() { .as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); + // count( distinct (all fields in icaoClass) + Class icaoClass = (new Object() { + String iata; + String icao; + }).getClass(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(REQUEST_PLUS).count(); + assertEquals(7, count2); + } finally { couchbaseTemplate.removeById() .all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); @@ -298,7 +315,8 @@ void distinctReactive() { assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) .as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); @@ -328,8 +346,8 @@ void sortedTemplate() { .query(QueryCriteria.where("iata").isNotNull()); Pageable pageableWithSort = PageRequest.of(0, 7, Sort.by("iata")); query.with(pageableWithSort); - List airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS) - .matching(query).all(); + List airports = couchbaseTemplate.findByQuery(Airport.class).matching(query).withConsistency(REQUEST_PLUS) + .all(); String[] sortedIatas = iatas.clone(); System.out.println("" + iatas.length + " " + sortedIatas.length); diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java index 8fd709ec5..265ad44df 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -37,6 +37,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.couchbase.core.ReactiveFindByIdOperation.ReactiveFindById; @@ -46,6 +48,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.PersonValue; import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware; import org.springframework.data.couchbase.domain.User; @@ -59,6 +62,7 @@ import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * KV tests Theses tests rely on a cb server running. @@ -67,8 +71,12 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(Config.class) class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @BeforeEach @Override public void beforeEach() { diff --git a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java index 42edc672a..26dafb8a0 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/ReactiveCouchbaseTemplateQueryCollectionIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core.query; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -36,10 +37,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; @@ -52,6 +56,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; @@ -64,7 +69,6 @@ import com.couchbase.client.java.kv.ReplaceOptions; import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryScanConsistency; /** * Query tests Theses tests rely on a cb server running This class tests collection support with @@ -74,10 +78,15 @@ * @author Michael Reiche */ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + @Autowired + public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + Airport vie = new Airport("airports::vie", "vie", "low80"); - ReactiveCouchbaseTemplate template = reactiveCouchbaseTemplate; + ReactiveCouchbaseTemplate template; @BeforeAll public static void beforeAll() { @@ -101,18 +110,16 @@ public void beforeEach() { // first call the super method super.beforeEach(); // then do processing for this class - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName) - .inCollection(collectionName).all(); - couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).all(); - couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).all(); + couchbaseTemplate.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).all(); + + template = reactiveCouchbaseTemplate; } @AfterEach @@ -121,8 +128,7 @@ public void afterEach() { // first do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); // query with REQUEST_PLUS to ensure that the remove has completed. - couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).all(); + couchbaseTemplate.findByQuery(User.class).inCollection(collectionName).withConsistency(REQUEST_PLUS).all(); // then call the super method super.afterEach(); } @@ -135,8 +141,8 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -178,8 +184,8 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); } @@ -203,8 +209,8 @@ void findByMatchingQueryProjected() { Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) - .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(daveUsers).all(); + .inCollection(collectionName).as(UserSubmissionProjected.class).matching(daveUsers) + .withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUserSubmissions.size()); assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); @@ -220,13 +226,13 @@ void findByMatchingQueryProjected() { couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); - final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).inCollection(collectionName) + .as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS).all(); assertEquals(1, foundUsers.size()); final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) - .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName) - .matching(specialUsers).all().collectList().block(); + .inCollection(collectionName).as(UserJustLastName.class).matching(specialUsers).withConsistency(REQUEST_PLUS) + .all().collectList().block(); assertEquals(1, foundUsersReactive.size()); } @@ -242,8 +248,8 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId())); assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId())); - List result = couchbaseTemplate.removeByQuery(User.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List result = couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).all(); assertEquals(2, result.size(), "should have deleted user1 and user2"); assertNull( @@ -267,8 +273,8 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inCollection(collectionName).matching(nonSpecialUsers).all(); + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).matching(nonSpecialUsers) + .withConsistency(REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); @@ -291,18 +297,18 @@ void distinct() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + List airports2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all(); assertEquals(7, airports2.size()); // count( distinct { iata, icao } ) - long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count1 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "iata", "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count1); // count( distinct (all fields in icaoClass) @@ -310,8 +316,8 @@ void distinct() { String iata; String icao; }).getClass(); - long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] {}) + .as(icaoClass).withConsistency(REQUEST_PLUS).count(); assertEquals(7, count2); } finally { @@ -335,26 +341,25 @@ void distinctReactive() { // as the fluent api for Distinct is tricky // distinct icao - List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList() + .block(); assertEquals(2, airports1.size()); // distinct all-fields-in-Airport.class - List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() - .collectList().block(); + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] {}).as(Airport.class).withConsistency(REQUEST_PLUS).all().collectList().block(); assertEquals(7, airports2.size()); // count( distinct icao ) - Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) - .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count() - .block(); + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName) + .distinct(new String[] { "icao" }).as(Airport.class).withConsistency(REQUEST_PLUS).count().block(); assertEquals(2, count1); // count( distinct { iata, icao } ) - Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block(); + Long count2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).inCollection(collectionName).distinct(new String[] { "iata", "icao" }) + .withConsistency(REQUEST_PLUS).count().block(); assertEquals(7, count2); } finally { @@ -427,8 +432,8 @@ public void findByQuery() { // 4 Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowa")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(scopeName).inCollection(collectionName).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).inScope(scopeName).inCollection(collectionName) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId()).block(); @@ -479,10 +484,9 @@ public void removeByQuery() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName) .one(vie.withIcao("lowe")).block(); - List removeResults = template.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList() - .block(); + List removeResults = template.removeByQuery(Airport.class).inScope(scopeName) + .inCollection(collectionName).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -526,13 +530,12 @@ public void existsByIdOther() { // 1 ExistsOptions existsOptions = ExistsOptions.existsOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowg")).block(); - try { Boolean exists = template.existsById().inScope(otherScope).inCollection(otherCollection) - .withOptions(existsOptions).one(vie.getId()).block(); - assertTrue(exists, "Airport should exist: " + vie.getId()); + .withOptions(existsOptions).one(saved.getId()).block(); + assertTrue(exists, "Airport should exist: " + saved.getId()); } finally { - template.removeById().inScope(otherScope).inCollection(otherCollection).one(vie.getId()).block(); + template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); } } @@ -571,8 +574,8 @@ public void findByQueryOther() { // 4 Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lowj")).block(); try { - List found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options).all().collectList().block(); + List found = template.findByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), found.get(0).getId()); } finally { template.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId()).block(); @@ -623,10 +626,9 @@ public void removeByQueryOther() { // 8 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10)); Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection) .one(vie.withIcao("lown")).block(); - List removeResults = template.removeByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection) - .withOptions(options).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList() - .block(); + List removeResults = template.removeByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))) + .withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block(); assertEquals(saved.getId(), removeResults.get(0).getId()); } @@ -689,9 +691,8 @@ public void findByIdOptions() { // 3 @Test public void findByQueryOptions() { // 4 QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); - assertThrows(AmbiguousTimeoutException.class, - () -> template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope) - .inCollection(otherCollection).withOptions(options).all().collectList().block()); + assertThrows(AmbiguousTimeoutException.class, () -> template.findByQuery(Airport.class).inScope(otherScope) + .inCollection(otherCollection).withConsistency(REQUEST_PLUS).withOptions(options).all().collectList().block()); } @Test @@ -729,9 +730,9 @@ public void removeByIdOptions() { // 7 - options public void removeByQueryOptions() { // 8 - options QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofNanos(10)); assertThrows(AmbiguousTimeoutException.class, - () -> template.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(otherScope).inCollection(otherCollection).withOptions(options) - .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block()); + () -> template.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection) + .matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).withConsistency(REQUEST_PLUS) + .withOptions(options).all().collectList().block()); } @Test diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index bb436d6b5..885d180ce 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -47,6 +47,7 @@ public class Airport extends ComparableEntity { @Expiration private long expiration; @Max(2) long size; + private long someNumber; @PersistenceConstructor public Airport(String key, String iata, String icao) { diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index 9a66f5efa..204382859 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -66,6 +66,7 @@ public interface AirportRepository extends CouchbaseRepository, List findByIataInAndIcaoIn(java.util.Collection size, java.util.Collection color, Pageable pageable); + // override an annotate with REQUEST_PLUS @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List findAll(); @@ -92,6 +93,12 @@ List findByIataInAndIcaoIn(java.util.Collection size, java.util @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Airport findByIataIn(JsonArray iatas); + @Query("Select \"\" AS __id, 0 AS __cas, substr(iata,0,1) as iata, count(*) as someNumber FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} GROUP BY substr(iata,0,1)") + List groupByIata(); + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Airport findArchivedByIata(Iata iata); + // NOT_BOUNDED to test ScanConsistency // @ScanConsistency(query = QueryScanConsistency.NOT_BOUNDED) Airport iata(String iata); diff --git a/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java new file mode 100644 index 000000000..ae3588c9b --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/CollectionsConfig.java @@ -0,0 +1,8 @@ +package org.springframework.data.couchbase.domain; + +public class CollectionsConfig extends Config { + @Override + public String getScopeName(){ + return "my_scope"; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 9e72e582f..debe255f4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -25,6 +25,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; import org.springframework.data.couchbase.cache.CouchbaseCacheConfiguration; import org.springframework.data.couchbase.cache.CouchbaseCacheManager; @@ -60,7 +61,7 @@ @EnableReactiveCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef") @EnableCaching public class Config extends AbstractCouchbaseConfiguration { - String bucketname = "travel-sample"; + String bucketname = "test"; String username = "Administrator"; String password = "password"; String connectionString = "127.0.0.1"; @@ -159,18 +160,16 @@ public void configureRepositoryOperationsMapping(RepositoryOperationsMapping bas // do not use reactiveCouchbaseTemplate for the name of this method, otherwise the value of that bean // will be used instead of the result of this call (the client factory arg is different) - public ReactiveCouchbaseTemplate myReactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { - return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, - new JacksonTranslationService(), getDefaultConsistency()); + public ReactiveCouchbaseTemplate myReactiveCouchbaseTemplate(ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return new ReactiveCouchbaseTemplate(reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), getDefaultConsistency()); } // do not use couchbaseTemplate for the name of this method, otherwise the value of that been // will be used instead of the result from this call (the client factory arg is different) - public CouchbaseTemplate myCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, - MappingCouchbaseConverter mappingCouchbaseConverter) { - return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), - getDefaultConsistency()); + public CouchbaseTemplate myCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return new CouchbaseTemplate(couchbaseClientFactory, reactiveCouchbaseClientFactory, mappingCouchbaseConverter, new JacksonTranslationService(), getDefaultConsistency()); } // do not use couchbaseClientFactory for the name of this method, otherwise the value of that bean will @@ -198,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 @@ -231,15 +230,14 @@ public String typeKey() { return "t"; // this will override '_class', is passed in to new CustomMappingCouchbaseConverter } - static String scopeName = null; - @Override protected String getScopeName() { return scopeName; } - public static void setScopeName(String scopeName) { + static public void setScopeName(String scopeName){ Config.scopeName = scopeName; } + static private String scopeName = null; } diff --git a/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java b/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java index 674f6124f..b16f2810f 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java +++ b/src/test/java/org/springframework/data/couchbase/domain/FluxTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors + * Copyright 2021 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.couchbase.domain; -import static org.junit.jupiter.api.Assertions.assertEquals; - +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryProfile; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.query.QueryScanConsistency; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.util.Pair; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.ParallelFlux; @@ -60,6 +73,8 @@ import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.query.QueryScanConsistency; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * @author Michael Reiche */ @@ -67,8 +82,13 @@ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class FluxTest extends JavaIntegrationTests { - @BeforeAll - public static void beforeEverything() { + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + + @BeforeEach + @Override + public void beforeEach() { + /** * The couchbaseTemplate inherited from JavaIntegrationTests uses org.springframework.data.couchbase.domain.Config * It has typeName = 't' (instead of _class). Don't use it. @@ -82,23 +102,19 @@ public static void beforeEverything() { couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection().upsert(k, JsonObject.create().put("x", k)); } + super.beforeEach(); } - @AfterAll - public static void afterEverthing() { + @AfterEach + public void afterEach() { couchbaseTemplate.removeByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + super.afterEach(); for (String k : keyList) { couchbaseTemplate.getCouchbaseClientFactory().getBucket().defaultCollection().remove(k); } } - @BeforeEach - @Override - public void beforeEach() { - super.beforeEach(); - } - static List keyList = Arrays.asList("a", "b", "c", "d", "e"); static Collection collection; static ReactiveCollection rCollection; diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index eb2a4dd75..376cdc7e4 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -22,13 +22,17 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.core.mapping.Field; +import org.springframework.data.couchbase.repository.TransactionResult; +import org.springframework.data.couchbase.repository.support.TransactionResultHolder; +import org.springframework.data.domain.Persistable; import org.springframework.lang.Nullable; @Document -public class Person extends AbstractEntity { +public class Person extends AbstractEntity implements Persistable { Optional firstname; @Nullable Optional lastname; @@ -47,6 +51,11 @@ public class Person extends AbstractEntity { private Address address; + // Required for use in transactions + @TransactionResult private Integer txResultHolder; + @Transient private boolean isNew; + + public Person() {} public Person(String firstname, String lastname) { @@ -54,6 +63,7 @@ public Person(String firstname, String lastname) { setFirstname(firstname); setLastname(lastname); setMiddlename("Nick"); + isNew(true); } public Person(int id, String firstname, String lastname) { @@ -61,19 +71,24 @@ public Person(int id, String firstname, String lastname) { setId(new UUID(id, id)); } + public Person(UUID id, String firstname, String lastname) { + this(firstname, lastname); + setId(id); + } + static String optional(String name, Optional obj) { if (obj != null) { if (obj.isPresent()) { - return (" " + name + ": '" + obj.get() + "'\n"); + return (" " + name + ": '" + obj.get() + "'"); } else { - return " " + name + ": null\n"; + return " " + name + ": null"; } } return ""; } - public Optional getFirstname() { - return firstname; + public String getFirstname() { + return firstname.get(); } public void setFirstname(String firstname) { @@ -84,8 +99,8 @@ public void setFirstname(Optional firstname) { this.firstname = firstname; } - public Optional getLastname() { - return lastname; + public String getLastname() { + return lastname.get(); } public void setLastname(String lastname) { @@ -131,7 +146,7 @@ public String toString() { sb.append(optional(", firstname", firstname)); sb.append(optional(", lastname", lastname)); if (middlename != null) - sb.append(", middlename : " + middlename); + sb.append(", middlename : '" + middlename + "'"); sb.append(", version : " + version); if (creator != null) { sb.append(", creator : " + creator); @@ -148,8 +163,43 @@ public String toString() { if (getAddress() != null) { sb.append(", address : " + getAddress().toString()); } - sb.append("}"); + sb.append("\n}"); return sb.toString(); } + public Person withFirstName(String firstName) { + Person p = new Person(this.getId(), firstName, this.getLastname()); + p.version = version; + p.txResultHolder = this.txResultHolder; + return p; + } + + public Person withVersion(Long version) { + //Person p = new Person(this.getId(), this.getFirstname(), this.getLastname()); + this.version = version; + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + + Person that = (Person) obj; + return this.getId().equals(that.getId()) && this.getFirstname().equals(that.getFirstname()) + && this.getLastname().equals(that.getLastname()) && this.getMiddlename().equals(that.getMiddlename()); + } + + @Override + public boolean isNew() { + return isNew; + } + + public void isNew(boolean isNew){ + this.isNew = isNew; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java index 2c9985d74..b6f23ebe2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -18,9 +18,10 @@ import java.util.List; import java.util.UUID; +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; -import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import com.couchbase.client.java.query.QueryScanConsistency; @@ -28,7 +29,7 @@ /** * @author Michael Reiche */ -public interface PersonRepository extends CrudRepository { +public interface PersonRepository extends CouchbaseRepository, DynamicProxyable { /* * These methods are exercised in HomeController of the test spring-boot DemoApplication @@ -95,7 +96,7 @@ public interface PersonRepository extends CrudRepository { boolean existsById(UUID var1); - Iterable findAll(); + List findAll(); long count(); 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/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index 59426ec27..a5851e8e6 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.domain; +import lombok.val; +import org.springframework.data.couchbase.core.query.WithConsistency; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,6 +46,11 @@ public interface ReactiveAirportRepository extends ReactiveCouchbaseRepository, DynamicProxyable { + + @Query("SELECT META(#{#n1ql.bucket}).id AS __id, META(#{#n1ql.bucket}).cas AS __cas, meta().id as id FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} #{[1]}") + @ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS) + Flux findIdByDynamicN1ql(String docType, String queryStatement); + @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Flux findAll(); diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java new file mode 100644 index 000000000..e28666513 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactivePersonRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; + +/** + * @author Michael Reiche + */ +public interface ReactivePersonRepository + extends ReactiveCouchbaseRepository, DynamicProxyable { + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 7143bb269..b946d0ab8 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -32,6 +32,9 @@ import java.util.Optional; import java.util.UUID; +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.java.env.ClusterEnvironment; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -221,6 +224,15 @@ public String getBucketName() { return bucketName(); } + @Override + protected void configureEnvironment(ClusterEnvironment.Builder builder) { + if(getConnectionString().contains("cloud.couchbase.com")) { + builder.securityConfig(SecurityConfig.builder() + .trustManagerFactory(InsecureTrustManagerFactory.INSTANCE) + .enableTls(true)); + } + } + } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index b24ff3aae..f648063d2 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -18,6 +18,8 @@ import static com.couchbase.client.java.query.QueryScanConsistency.NOT_BOUNDED; import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static com.couchbase.client.java.query.QueryOptions.queryOptions; +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -107,6 +109,8 @@ import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.kv.InsertOptions; import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.kv.UpsertOptions; import com.couchbase.client.java.query.QueryScanConsistency; /** @@ -137,7 +141,10 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr public void beforeEach() { super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).all(); + couchbaseTemplate.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + } @Test @@ -277,8 +284,7 @@ void findBySimpleProperty() { try { vie = new Airport("airports::vie", "vie", "low6"); vie = airportRepository.save(vie); - Airport airport2 = airportRepository - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + Airport airport2 = airportRepository.withOptions(queryOptions().scanConsistency(REQUEST_PLUS)) .findByIata(vie.getIata()); assertEquals(airport2, vie); @@ -378,7 +384,7 @@ public void saveRequestPlusWithDefaultRepository() { Airport vie = new Airport("airports::vie", "vie", "low9"); Airport saved = airportRepositoryRP.save(vie); - List allSaved = airportRepositoryRP.findAll(); + List allSaved = airportRepositoryRP.findAll(REQUEST_PLUS); couchbaseTemplate.removeById(Airport.class).one(saved.getId()); assertEquals(1, allSaved.size(), "should have found 1 airport"); } @@ -390,9 +396,10 @@ void findByTypeAlias() { vie = new Airport("airports::vie", "vie", "loww"); vie = airportRepository.save(vie); List airports = couchbaseTemplate.findByQuery(Airport.class) - .withConsistency(QueryScanConsistency.REQUEST_PLUS) .matching(org.springframework.data.couchbase.core.query.Query .query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))) + .withConsistency(REQUEST_PLUS) + .all(); assertFalse(airports.isEmpty(), "should have found aiport"); } finally { @@ -452,15 +459,13 @@ void findBySimplePropertyWithCollection() { Airport saved = airportRepository.withScope(scopeName).withCollection(collectionName).save(vie); // given collection (on scope used by template) Airport airport2 = airportRepository.withCollection(collectionName) - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) - .iata(vie.getIata()); + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS)).iata(vie.getIata()); assertEquals(saved, airport2); // given scope and collection Airport airport3 = airportRepository.withScope(scopeName).withCollection(collectionName) - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) - .iata(vie.getIata()); + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS)).iata(vie.getIata()); assertEquals(saved, airport3); // given bad collection @@ -468,7 +473,8 @@ void findBySimplePropertyWithCollection() { () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata())); // given bad scope - assertThrows(IndexFailureException.class, () -> airportRepository.withScope("bogusScope").iata(vie.getIata())); + assertThrows(IndexFailureException.class, + () -> airportRepository.withScope("bogusScope").withCollection(collectionName).iata(vie.getIata())); } finally { airportRepository.delete(vie); @@ -502,12 +508,11 @@ void findBySimplePropertyWithOptions() { try { Airport saved = airportRepository.save(vie); // Duration of 1 nano-second will cause timeout - assertThrows(AmbiguousTimeoutException.class, () -> airportRepository - .withOptions(QueryOptions.queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); + assertThrows(AmbiguousTimeoutException.class, + () -> airportRepository.withOptions(queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); - Airport airport3 = airportRepository.withOptions( - QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) - .iata(vie.getIata()); + Airport airport3 = airportRepository + .withOptions(queryOptions().scanConsistency(REQUEST_PLUS).parameters(positionalParams)).iata(vie.getIata()); assertEquals(saved, airport3); } finally { @@ -525,7 +530,8 @@ public void saveNotBounded() { // set version == 0 so save() will be an upsert, not a replace Airport saved = airportRepository.save(vie.clearVersion()); try { - airport2 = airportRepository.iata(saved.getIata()); + airport2 = airportRepository.withOptions(queryOptions().scanConsistency(QueryScanConsistency.NOT_BOUNDED)) + .iata(saved.getIata()); if (airport2 == null) { break; } @@ -538,7 +544,8 @@ public void saveNotBounded() { assertEquals(vie.getId(), removeResult.getId()); assertTrue(removeResult.getCas() != 0); assertTrue(removeResult.getMutationToken().isPresent()); - Airport airport3 = airportRepository.iata(vie.getIata()); + Airport airport3 = airportRepository.withOptions(queryOptions().scanConsistency(REQUEST_PLUS) + .consistentWith(MutationState.from(removeResult.getMutationToken().get()))).iata(vie.getIata()); assertNull(airport3, "should have been removed"); } } @@ -705,6 +712,24 @@ void countSlicePage() { } } + @Test + void testGroupBy() { + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + try { + airportRepository.saveAll( + Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) + .collect(Collectors.toSet())); + List airports = airportRepository.groupByIata(); + for (Airport a : airports) { + System.out.println(a); + } + + } finally { + airportRepository + .deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); + } + } + @Test void badCount() { assertThrows(CouchbaseQueryExecutionException.class, () -> airportRepository.countBad()); @@ -856,11 +881,11 @@ void threadSafeStringParametersTest() throws Exception { } @Test - // DATACOUCH-650 + // DATACOUCH-650 void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDZ"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { airportRepository.saveAll(asList(vienna, frankfurt, losAngeles)); @@ -875,8 +900,8 @@ void deleteAllById() { void couchbaseRepositoryQuery() throws Exception { User user = new User("1", "Dave", "Wilson"); userRepository.save(user); - couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS) - .matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")).all(); + couchbaseTemplate.findByQuery(User.class).matching(QueryCriteria.where("firstname").is("Dave").and("`1`").is("`1`")) + .withConsistency(REQUEST_PLUS).all(); String input = "findByFirstname"; Method method = UserRepository.class.getMethod(input, String.class); CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index 725a93b6a..f80a37204 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -92,6 +92,28 @@ void shouldSaveAndFindAll() { } } + @Test + void testQuery() { + Airport vie = null; + Airport jfk = null; + try { + vie = new Airport("airports::vie", "vie", "low1"); + airportRepository.save(vie).block(); + jfk = new Airport("airports::jfk", "JFK", "xxxx"); + airportRepository.save(jfk).block(); + + List all = airportRepository.findIdByDynamicN1ql("","").toStream().collect(Collectors.toList()); + System.out.println(all); + assertFalse(all.isEmpty()); + assertTrue(all.stream().anyMatch(a -> a.equals("airports::vie"))); + assertTrue(all.stream().anyMatch(a -> a.equals("airports::jfk"))); + + } finally { + airportRepository.delete(vie).block(); + airportRepository.delete(jfk).block(); + } + } + @Test void findBySimpleProperty() { Airport vie = null; @@ -215,7 +237,7 @@ void count() { void deleteAllById() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDX"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { @@ -235,7 +257,7 @@ void deleteAllById() { void deleteAll() { Airport vienna = new Airport("airports::vie", "vie", "LOWW"); - Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); + Airport frankfurt = new Airport("airports::fra", "fra", "EDDY"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); try { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index 748401ce3..44e51ecce 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -32,10 +32,14 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.AddressAnnotated; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportRepository; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserCol; @@ -48,6 +52,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexFailureException; import com.couchbase.client.core.io.CollectionIdentifier; @@ -56,12 +61,16 @@ import com.couchbase.client.java.query.QueryScanConsistency; @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { - @Autowired AirportRepository airportRepository; // initialized in beforeEach() - @Autowired UserColRepository userColRepository; // initialized in beforeEach() - @Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; // initialized in beforeEach() - @Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; // initialized in beforeEach() + @Autowired AirportRepository airportRepository; + @Autowired UserColRepository userColRepository; + @Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; + @Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; + + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @BeforeAll public static void beforeAll() { @@ -86,14 +95,10 @@ public void beforeEach() { // then do processing for this class couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); couchbaseTemplate.removeByQuery(UserCol.class).inScope(otherScope).inCollection(otherCollection).all(); - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); // seems that @Autowired is not adequate, so ... - airportRepository = (AirportRepository) ac.getBean("airportRepository"); - userColRepository = (UserColRepository) ac.getBean("userColRepository"); - userSubmissionAnnotatedRepository = (UserSubmissionAnnotatedRepository) ac - .getBean("userSubmissionAnnotatedRepository"); - userSubmissionUnannotatedRepository = (UserSubmissionUnannotatedRepository) ac - .getBean("userSubmissionUnannotatedRepository"); + // airportRepository = (AirportRepository) ac.getBean("airportRepository"); + // userColRepository = (UserColRepository) ac.getBean("userColRepository"); } @AfterEach @@ -222,9 +227,14 @@ public void testScopeCollectionAnnotationSwap() { // collection from CrudMethodMetadata of UserCol.save() UserCol userCol = new UserCol("1", "Dave", "Wilson"); Airport airport = new Airport("3", "myIata", "myIcao"); - UserCol savedCol = userColRepository.save(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata - userColRepository.delete(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata - assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); + try { + UserCol savedCol = userColRepository.save(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata + userColRepository.delete(userCol); // uses UserCol annotation scope, populates CrudMethodMetadata + assertThrows(IllegalStateException.class, () -> airportRepository.save(airport)); + } finally { + List removed = couchbaseTemplate.removeByQuery(Airport.class).all(); + couchbaseTemplate.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } } // template default scope is my_scope @@ -278,8 +288,8 @@ void findPlusN1qlJoinBothAnnotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(scopeName).all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionAnnotatedRepository.findByUsername(user.getUsername()); @@ -333,8 +343,8 @@ void findPlusN1qlJoinUnannotated() throws Exception { address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1); address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2); address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3); - couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) - .inScope(scopeName).all(); + couchbaseTemplate.findByQuery(AddressAnnotated.class).inScope(scopeName).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .all(); // scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository. List users = userSubmissionUnannotatedRepository.findByUsername(user.getUsername()); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java index 51f6a5efe..0318d4634 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -29,7 +29,10 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.CollectionsConfig; import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.ReactiveAirportRepository; import org.springframework.data.couchbase.domain.ReactiveUserColRepository; @@ -39,6 +42,7 @@ import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexFailureException; import com.couchbase.client.core.io.CollectionIdentifier; @@ -47,10 +51,13 @@ import com.couchbase.client.java.query.QueryScanConsistency; @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CollectionsConfig.class) public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { @Autowired ReactiveAirportRepository airportRepository; @Autowired ReactiveUserColRepository userColRepository; + @Autowired public CouchbaseTemplate couchbaseTemplate; + @Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; @BeforeAll public static void beforeAll() { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index 72f657d2c..50c663666 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -23,7 +23,6 @@ import java.util.Properties; import java.util.UUID; -import com.couchbase.client.java.query.QueryScanConsistency; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; @@ -58,6 +57,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * @author Michael Nitschinger * @author Michael Reiche @@ -99,8 +100,8 @@ queryMethod, converter, config().bucketname(), new SpelExpressionParser(), Query query = creator.createQuery(); - ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate - .findByQuery(Airline.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query); + ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate.findByQuery(Airline.class).matching(query) + .withConsistency(QueryScanConsistency.REQUEST_PLUS); Optional al = q.one(); assertEquals(airline.toString(), al.get().toString()); diff --git a/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java new file mode 100644 index 000000000..1771e909e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/AfterTransactionAssertion.java @@ -0,0 +1,33 @@ +package org.springframework.data.couchbase.transactions; + +import lombok.Data; + +import org.springframework.data.domain.Persistable; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ +@Data +public class AfterTransactionAssertion { + + private final T persistable; + private boolean expectToBePresent; + + public void isPresent() { + expectToBePresent = true; + } + + public void isNotPresent() { + expectToBePresent = false; + } + + public Object getId() { + return persistable.getId(); + } + + public boolean shouldBePresent() { + return expectToBePresent; + } +} + diff --git a/src/test/java/org/springframework/data/couchbase/transactions/Config.java b/src/test/java/org/springframework/data/couchbase/transactions/Config.java new file mode 100644 index 000000000..e9193f294 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/Config.java @@ -0,0 +1,74 @@ +package org.springframework.data.couchbase.transactions; + +import java.time.Duration; + +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.transaction.config.CoreTransactionsConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.couchbase.client.java.transactions.config.TransactionOptions; + +@Configuration +@EnableCouchbaseRepositories("org.springframework.data.couchbase") +@EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") +@EnableTransactionManagement +class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return ClusterAwareIntegrationTests.connectionString(); + } + + @Override + public String getUserName() { + return ClusterAwareIntegrationTests.config().adminUsername(); + } + + @Override + public String getPassword() { + return ClusterAwareIntegrationTests.config().adminPassword(); + } + + @Override + public String getBucketName() { + return ClusterAwareIntegrationTests.bucketName(); + } + + @Override + public TransactionsConfig.Builder transactionsConfig() { + return TransactionsConfig.builder().durabilityLevel(DurabilityLevel.NONE).timeout(Duration.ofMinutes(20));// for testing + } + + /* + @Override + public TransactionsConfig transactionConfig() { + // expirationTime 20 minutes for stepping with the debugger + return TransactionsConfig.create() + .logDirectly(Event.Severity.INFO) + .logOnFailure(true, + Event.Severity.ERROR) + .expirationTime(Duration.ofMinutes(20)) + .durabilityLevel(TransactionDurabilityLevel.MAJORITY) + .build(); + } + */ + /* + beforeAll creates a PersonService bean in the applicationContext + + context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionIntegrationTests.Config.class, + PersonService.class); + + @Bean("personService") + PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + return new PersonService(ops, mgr, opsRx, mgrRx); + } + */ + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java new file mode 100644 index 000000000..db8b35d40 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionIntegrationTests.java @@ -0,0 +1,889 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; + +import com.couchbase.client.core.transaction.CoreTransactionAttemptContext; +import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor; +import com.couchbase.client.java.transactions.config.TransactionOptions; +import lombok.Data; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.ReactiveCouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionDefinition; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseResourceHolder; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.ReactiveTransactionsWrapper; +import org.springframework.data.couchbase.transaction.TransactionsWrapper; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +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 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; + +/** + * Tests for com.couchbase.transactions using + *
  • couchbase reactive transaction manager via transactional operator couchbase non-reactive transaction + * manager via @Transactional @Transactional(transactionManager = + * BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER)
  • + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(classes = { Config.class, CouchbasePersonTransactionIntegrationTests.PersonService.class }) +public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseClientFactory reactiveCouchbaseClientFactory; + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired CouchbaseTemplate cbTmpl; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + @Autowired PersonService personService; + @Autowired CouchbaseTemplate operations; + + // if these are changed from default, then beforeEach needs to clean up separately + String sName = "_default"; + String cName = "_default"; + private TransactionalOperator transactionalOperator; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + List rp0 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List rp1 = operations.removeByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + List rp2 = operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List rp3 = operations.removeByQuery(EventLog.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + + List p0 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + List p1 = operations.findByQuery(Person.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + List e0 = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); + List e1 = operations.findByQuery(EventLog.class).inScope(sName).inCollection(cName) + .withConsistency(REQUEST_PLUS).all(); + + Person walterWhite = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, walterWhite.getId().toString()); + transactionalOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager); + } + + @Test + public void shouldRollbackAfterException() { + Person p = new Person(null, "Walter", "White"); + assertThrows(SimulateFailureException.class, () -> personService.savePersonErrors(p)); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { + Person p = new Person(null, "Walter", "White"); + assertThrowsOneOf(() -> personService.declarativeSavePersonErrors(p), TransactionFailedException.class); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void shouldRollbackAfterExceptionOfTxAnnotatedMethodReactive() { + Person p = new Person(null, "Walter", "White"); + assertThrows(SimulateFailureException.class, () -> personService.declarativeSavePersonErrorsReactive(p).block()); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(0, count, "should have done roll back and left 0 entries"); + } + + @Test + public void commitShouldPersistTxEntries() { + Person p = new Person(null, "Walter", "White"); + Person s = personService.savePerson(p); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { + Person p = new Person(null, "Walter", "White"); + Person s = personService.declarativeSavePerson(p); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void replaceInTxAnnotatedCallback() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + cbTmpl.insertById(Person.class).one(person); + AtomicInteger tryCount = new AtomicInteger(0); + Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + @Test + public void commitShouldPersistTxEntriesOfTxAnnotatedMethodReactive() { + Person p = new Person(null, "Walter", "White"); + Person s = personService.declarativeSavePersonReactive(p).block(); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + @Test + public void commitShouldPersistTxEntriesAcrossCollections() { + List persons = personService.saveWithLogs(new Person(null, "Walter", "White")); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + Long countEvents = operations.count(new Query(), EventLog.class); // + assertEquals(4, countEvents, "should have saved and found 4"); + } + + @Test + public void rollbackShouldAbortAcrossCollections() { + assertThrows(SimulateFailureException.class, + () -> personService.saveWithErrorLogs(new Person(null, "Walter", "White"))); + List persons = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all(); + assertEquals(0, persons.size(), "should have done roll back and left 0 entries"); + List events = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all(); // + assertEquals(0, events.size(), "should have done roll back and left 0 entries"); + } + + @Test + public void countShouldWorkInsideTransaction() { + Long count = personService.countDuringTx(new Person(null, "Walter", "White")); + assertEquals(1, count, "should have counted 1 during tx"); + } + + @Test + public void emitMultipleElementsDuringTransaction() { + List docs = personService.saveWithLogs(new Person(null, "Walter", "White")); + assertEquals(4, docs.size(), "should have found 4 eventlogs"); + } + + @Test + public void errorAfterTxShouldNotAffectPreviousStep() { + Person p = personService.savePerson(new Person(null, "Walter", "White")); + // todo gp user shouldn't be getting exposed to TransactionOperationFailedException + // todo mr + /* + TransactionOperationFailedException {cause:com.couchbase.client.core.error.DocumentExistsException, retry:false, autoRollback:true, raise:TRANSACTION_FAILED} + at com.couchbase.client.core.error.transaction.TransactionOperationFailedException$Builder.build(TransactionOperationFailedException.java:136) + at com.couchbase.client.core.transaction.CoreTransactionAttemptContext.lambda$handleDocExistsDuringStagedInsert$116(CoreTransactionAttemptContext.java:1801) + */ + assertThrows(TransactionOperationFailedException.class, () -> personService.savePerson(p)); + Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count(); + assertEquals(1, count, "should have saved and found 1"); + } + + /** + * This will appear to work even if replaceById does not use a transaction. + */ + @Test + @Disabled + public void replacePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + cbTmpl.insertById(Person.class).one(person); + Mono result = rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap((pp) -> rxCBTmpl.replaceById(Person.class).one(pp)) // + .as(transactionalOperator::transactional); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertEquals(person, pFound, "should have found expected " + person); + } + + @Test + public void insertPersonCBTransactionsRxTmplRollback() { + Person person = new Person(1, "Walter", "White"); + Mono result = rxCBTmpl.insertById(Person.class).one(person) // + .flatMap(p -> throwSimulatedFailure(p)).as(transactionalOperator::transactional); // tx + assertThrows(SimulateFailureException.class, result::block); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertNull(pFound, "insert should have been rolled back"); + } + + @Test + public void insertTwicePersonCBTransactionsRxTmplRollback() { + Person person = new Person(1, "Walter", "White"); + Mono result = rxCBTmpl.insertById(Person.class).one(person) // + .flatMap((ppp) -> rxCBTmpl.insertById(Person.class).one(ppp)) // + .as(transactionalOperator::transactional); + assertThrows(DuplicateKeyException.class, result::block); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertNull(pFound, "insert should have been rolled back"); + } + + /** + * This test has the bare minimum for reactive transactions. Create the ClientSession that holds the ctx and put it in + * a resourceHolder and binds it to the currentContext. The retries are handled by couchbase-transactions - which + * creates a new ctx and re-runs the lambda. This is essentially what TransactionWrapper does. + */ + @Test + public void wrapperReplaceWithCasConflictResolvedViaRetry() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(0); + cbTmpl.insertById(Person.class).one(person); + + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); + t.start(); + tryCount.set(0); + TransactionsWrapper transactionsWrapper = new TransactionsWrapper(couchbaseClientFactory); + TransactionResult txResult = transactionsWrapper.run(ctx -> { + System.err.println("try: " + tryCount.incrementAndGet()); + Person ppp = cbTmpl.findById(Person.class).one(person.getId().toString()); + Person pppp = cbTmpl.replaceById(Person.class).one(ppp); + }); + System.out.println("txResult: " + txResult); + t.setStopFlag(); + if (tryCount.get() > 1) { + break; + } + } + Person pFound = cbTmpl.findById(Person.class).one(person.getId().toString()); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + @Test + public void wrapperReplaceWithCasConflictResolvedViaRetryReactive() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(0); + cbTmpl.insertById(Person.class).one(person); + + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); + t.start(); + tryCount.set(0); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { + System.err.println("try: " + tryCount.incrementAndGet()); + return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap((ppp) -> rxCBTmpl.replaceById(Person.class).one(ppp)).then(); + }); + TransactionResult txResult = result.block(); + System.out.println("txResult: " + txResult); + t.setStopFlag(); + if (tryCount.get() > 1) { + break; + } + } + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + } + + /** + * This does process retries - by CallbackTransactionManager.execute() -> transactions.run() -> executeTransaction() + * -> retryWhen. + */ + @Test + public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + AtomicInteger tryCount = new AtomicInteger(0); + cbTmpl.insertById(Person.class).one(person); + + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); + t.start(); + tryCount.set(0); + Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount); + t.setStopFlag(); + if (tryCount.get() > 1) { + break; + } + } + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + } + + /** + * Reactive @Transactional does not retry write-write conflicts. It throws RetryTransactionException up to the client + * and expects the client to retry. + */ + @Test + public void replaceWithCasConflictResolvedViaRetryAnnotatedReactive() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + cbTmpl.insertById(Person.class).one(person); + AtomicInteger tryCount = new AtomicInteger(0); + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); + t.start(); + tryCount.set(0); + // TODO mr - Graham says not to do delegate retries to user. He's a party pooper. + Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount) + .retryWhen(Retry.backoff(10, Duration.ofMillis(50)) + .filter(throwable -> throwable instanceof TransactionOperationFailedException) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + throw new RuntimeException("Transaction failed after max retries"); + })) + .block(); + t.setStopFlag(); + if (tryCount.get() > 1) { + break; + } + } + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + } + + @Test + /** + * This fails with TransactionOperationFailed {ec:FAIL_CAS_MISMATCH, retry:true, autoRollback:true}. I don't know why + * it isn't retried. This seems like it is due to the functioning of AbstractPlatformTransactionManager + */ + public void replaceWithCasConflictResolvedViaRetryAnnotated() { + Person person = new Person(1, "Walter", "White"); + Person switchedPerson = new Person(1, "Dave", "Reynolds"); + cbTmpl.insertById(Person.class).one(person); + AtomicInteger tryCount = new AtomicInteger(0); + + for (int i = 0; i < 50; i++) { // the transaction sometimes succeeds on the first try + ReplaceLoopThread t = new ReplaceLoopThread(switchedPerson); + t.start(); + tryCount.set(0); + Person p = personService.declarativeFindReplacePerson(person, tryCount); + t.setStopFlag(); + if (tryCount.get() > 1) { + break; + } + } + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + System.out.println("pFound: " + pFound); + assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched"); + assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get()); + } + + @Test + public void replacePersonCBTransactionsRxTmplRollback() { + Person person = new Person(1, "Walter", "White"); + String newName = "Walt"; + rxCBTmpl.insertById(Person.class).one(person).block(); + + // doesn't TransactionWrapper do the same thing? + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // + return rxCBTmpl.findById(Person.class).one(person.getId().toString()) // + .flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName(newName))).then(Mono.empty()); + }); + + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).one(person.getId().toString()).block(); + System.err.println(pFound); + assertEquals(newName, pFound.getFirstname()); + } + + @Test + public void deletePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person).then(); + }); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxTmplFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person) + .then(rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person)).then(); + }); + assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void deletePersonCBTransactionsRxRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx + return rxRepo.withCollection(cName).delete(person).then(); + }); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxRepoFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> { // get the ctx + return rxRepo.withCollection(cName).findById(person.getId().toString()) + .flatMap(pp -> rxRepo.withCollection(cName).delete(pp).then(rxRepo.withCollection(cName).delete(pp))).then(); + }); + assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person + " instead of " + pFound); + } + + @Test + public void findPersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class) + .inScope(sName).inCollection(cName).matching(q).withConsistency(REQUEST_PLUS).one().doOnSuccess(doc -> { + System.err.println("doc: " + doc); + docs.add(doc); + })); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person + " instead of " + o); + } + } + + @Test + public void insertPersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper + .run(ctx -> rxCBTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person) + . flatMap(it -> Mono.error(new SimulateFailureException()))); + + assertThrowsWithCause(() -> result.block(), TransactionFailedException.class, SimulateFailureException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void replacePersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> // + rxCBTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.getId().toString()) // + .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inScope(sName).inCollection(cName) + .one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new SimulateFailureException()))); + assertThrowsWithCause(() -> result.block(), TransactionFailedException.class, SimulateFailureException.class); + + Person pFound = cbTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person + " instead of " + pFound); + } + + @Test + public void findPersonSpringTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, sName, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + ReactiveTransactionsWrapper reactiveTransactionsWrapper = new ReactiveTransactionsWrapper( + reactiveCouchbaseClientFactory); + Mono result = reactiveTransactionsWrapper.run(ctx -> rxCBTmpl.findByQuery(Person.class) + .inScope(sName).inCollection(cName).matching(q).one().doOnSuccess(r -> docs.add(r))); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + private class ReplaceLoopThread extends Thread { + AtomicBoolean stop = new AtomicBoolean(false); + Person person; + int maxIterations = 100; + + public ReplaceLoopThread(Person person, int... iterations) { + this.person = person; + if (iterations != null && iterations.length == 1) { + this.maxIterations = iterations[0]; + } + } + + public void run() { + for (int i = 0; i < maxIterations && !stop.get(); i++) { + sleepMs(10); + try { + // note that this does not go through spring-data, therefore it does not have the @Field , @Version etc. + // annotations processed so we just check getFirstname().equals() + // switchedPerson has version=0, so it doesn't check CAS + couchbaseClientFactory.getBucket().defaultCollection().replace(person.getId().toString(), person); + System.out.println("********** replace thread: " + i + " success"); + } catch (Exception e) { + System.out.println("********** replace thread: " + i + " " + e.getClass().getName()); + } + } + + } + + public void setStopFlag() { + stop.set(true); + } + } + + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String scope, String collection, String id) { + remove(template.reactive(), scope, collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String scope, String collection, String id) { + try { + template.removeById(Person.class).inScope(scope).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + List ps = template.findByQuery(Person.class).inScope(scope).inCollection(collection) + .withConsistency(REQUEST_PLUS).all().collectList().block(); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + private Mono throwSimulatedFailure(T p) { + throw new SimulateFailureException(); + } + + @Data + static class EventLog { + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + String id; + String action; + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("EventLog : {\n"); + sb.append(" id : " + getId()); + sb.append(", action: " + action); + return sb.toString(); + } + } + + // todo gp disabled while trying to get alternative method of CouchbaseCallbackTransactionManager working + // @Configuration(proxyBeanMethods = false) + // @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + // static class TransactionInterception { + // + // @Bean + // @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + // public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource, + // CouchbaseTransactionManager txManager) { + // TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(); + // interceptor.setTransactionAttributeSource(transactionAttributeSource); + // if (txManager != null) { + // interceptor.setTransactionManager(txManager); + // } + // return interceptor; + // } + // + // @Bean + // @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + // public TransactionAttributeSource transactionAttributeSource() { + // return new AnnotationTransactionAttributeSource(); + // } + // + // @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) + // @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + // public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( + // TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { + // + // BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); + // advisor.setTransactionAttributeSource(transactionAttributeSource); + // advisor.setAdvice(transactionInterceptor); + // // if (this.enableTx != null) { + // // advisor.setOrder(this.enableTx.getNumber("order")); + // // } + // return advisor; + // } + // + // } + + @Service + @Component + @EnableTransactionManagement + static class PersonService { + + final CouchbaseOperations personOperations; + final CouchbaseSimpleCallbackTransactionManager manager; // final ReactiveCouchbaseTransactionManager manager; + final ReactiveCouchbaseOperations personOperationsRx; + final ReactiveCouchbaseTransactionManager managerRx; + + public PersonService(CouchbaseOperations ops, CouchbaseSimpleCallbackTransactionManager mgr, + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + personOperations = ops; + manager = mgr; + System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); + // System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); + System.err.println("manager Manager : " + manager); + + personOperationsRx = opsRx; + managerRx = mgrRx; + System.out + .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); + System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); + System.out.println("managerRx Manager : " + managerRx); + return; + } + + public Person savePersonErrors(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class).one(person)// + . flatMap(it -> Mono.error(new SimulateFailureException()))// + .as(transactionalOperator::transactional).block(); + } + + public Person savePerson(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + return personOperationsRx.insertById(Person.class).one(person)// + .as(transactionalOperator::transactional).block(); + } + + public Long countDuringTx(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + return personOperationsRx.insertById(Person.class).one(person)// + .then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) + .as(transactionalOperator::transactional).block(); + } + + public List saveWithLogs(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux + .merge(personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.insertById(Person.class).one(person), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // + .as(transactionalOperator::transactional).collectList().block(); + + } + + public List saveWithErrorLogs(Person person) { + assertInAnnotationTransaction(false); + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux + .merge(personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.insertById(Person.class).one(person), // + personOperationsRx.insertById(EventLog.class).one(new EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // + . flatMap(it -> Mono.error(new SimulateFailureException())).as(transactionalOperator::transactional) + .collectList().block(); + + } + + // org.springframework.beans.factory.NoUniqueBeanDefinitionException: + // No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single + // matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeSavePerson(Person person) { + assertInAnnotationTransaction(true); + return personOperations.insertById(Person.class).one(person); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeSavePersonErrors(Person person) { + assertInAnnotationTransaction(true); + Person p = personOperations.insertById(Person.class).one(person); // + SimulateFailureException.throwEx(); + return p; + } + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeFindReplacePersonCallback(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).one(person.getId().toString()); + return personOperations.replaceById(Person.class).one(p.withFirstName(person.getFirstname())); + } + + /** + * The ReactiveCouchbaseTransactionManager does not retry on write-write conflict. Instead it will throw + * RetryTransactionException to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + public Mono declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePersonReactive try: " + tryCount.incrementAndGet()); + return personOperationsRx.findById(Person.class).one(person.getId().toString()) + .flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(person.getFirstname()))); + } + + /** + * to execute while ThreadReplaceloop() is running should force a retry + * + * @param person + * @return + */ + @Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) + public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) { + assertInAnnotationTransaction(true); + System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet()); + Person p = personOperations.findById(Person.class).one(person.getId().toString()); + return personOperations.replaceById(Person.class).one(p); + } + + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) // doesn't retry + public Mono declarativeSavePersonReactive(Person person) { + assertInAnnotationTransaction(true); + return personOperationsRx.insertById(Person.class).one(person); + } + + @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER) + public Mono declarativeSavePersonErrorsReactive(Person person) { + assertInAnnotationTransaction(true); + Mono p = personOperationsRx.insertById(Person.class).one(person); // + SimulateFailureException.throwEx(); + return p; + } + + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java new file mode 100644 index 000000000..b92bd331b --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbasePersonTransactionReactiveIntegrationTests.java @@ -0,0 +1,612 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.transactions; + +import com.couchbase.client.java.Cluster; +import lombok.Data; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.annotation.Version; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.transaction.CouchbaseSimpleCallbackTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.domain.Persistable; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import com.couchbase.client.core.cnc.Event; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbasePersonTransactionReactiveIntegrationTests.Config.class) +//@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) +public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + @Autowired CouchbaseSimpleCallbackTransactionManager couchbaseTransactionManager; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + + @Autowired Cluster myCluster; + + /* DO NOT @Autowired */ PersonService personService; + + static GenericApplicationContext context; + @Autowired ReactiveCouchbaseTemplate operations; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + context = new AnnotationConfigApplicationContext(CouchbasePersonTransactionReactiveIntegrationTests.Config.class, + CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + public void beforeEachTest() { + personService = context.getBean(CouchbasePersonTransactionReactiveIntegrationTests.PersonService.class); // getting it via autowired results in no @Transactional + operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block(); + } + + + @Test // DATAMONGO-2265 + public void shouldRollbackAfterException() { + personService.savePersonErrors(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .verifyError(RuntimeException.class); + // operations.findByQuery(Person.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); + // sleepMs(5000); + operations.count(new Query(), Person.class) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + // @Rollback(false) + public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() { + Person p = new Person(null, "Walter", "White"); + try { + personService.declarativeSavePersonErrors(p) // + .as(StepVerifier::create) // + .expectComplete(); + // .verifyError(RuntimeException.class); + } catch (RuntimeException e) { + if (e instanceof SimulateFailureException || (e.getMessage() != null && e.getMessage().contains("poof"))) { + System.err.println(e); + } else { + throw e; + } + } + + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntries() { + + personService.savePerson(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count().block(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() { + + personService.declarativeSavePerson(new Person(null, "Walter", "White")).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + } + + @Test // DATAMONGO-2265 + public void commitShouldPersistTxEntriesAcrossCollections() { + + personService.saveWithLogs(new Person(null, "Walter", "White")) // + .then() // + .as(StepVerifier::create) // + .verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(4L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void rollbackShouldAbortAcrossCollections() { + + personService.saveWithErrorLogs(new Person(null, "Walter", "White")) // + .then() // + .as(StepVerifier::create) // + .verifyError(); + + operations.count(new Query(), Person.class) // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + + operations.count(new Query(), EventLog.class)// + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void countShouldWorkInsideTransaction() { + + personService.countDuringTx(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test // DATAMONGO-2265 + public void emitMultipleElementsDuringTransaction() { + + try { + personService.saveWithLogs(new Person(null, "Walter", "White")) // + .as(StepVerifier::create) // + .expectNextCount(4L) // + .verifyComplete(); + } catch (Exception e) { + System.err.println("Done"); + throw e; + } + } + + @Test // DATAMONGO-2265 + public void errorAfterTxShouldNotAffectPreviousStep() { + + Person p = new Person(1, "Walter", "White"); + //remove(couchbaseTemplate, "_default", p.getId().toString()); + personService.savePerson(p) // + //.delayElement(Duration.ofMillis(100)) // + .then(Mono.error(new RuntimeException("my big bad evil error"))).as(StepVerifier::create) // + .expectError() + .verify(); + //.expectError() // + //.as(StepVerifier::create) + //.expectNext(p) + //.verifyComplete(); + + operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + // @RequiredArgsConstructor + static class PersonService { + + final ReactiveCouchbaseOperations personOperationsRx; + final ReactiveCouchbaseTransactionManager managerRx; + final CouchbaseOperations personOperations; + //final CouchbaseCallbackTransactionManager manager; + + public PersonService(CouchbaseOperations ops, /* CouchbaseCallbackTransactionManager mgr, */ ReactiveCouchbaseOperations opsRx, + ReactiveCouchbaseTransactionManager mgrRx) { + personOperations = ops; + //manager = mgr; + System.err.println("operations cluster : " + personOperations.getCouchbaseClientFactory().getCluster()); +// System.err.println("manager cluster : " + manager.getDatabaseFactory().getCluster()); + //System.err.println("manager Manager : " + manager); + + personOperationsRx = opsRx; + managerRx = mgrRx; + System.out + .println("operationsRx cluster : " + personOperationsRx.getCouchbaseClientFactory().getCluster().block()); + System.out.println("managerRx cluster : " + mgrRx.getDatabaseFactory().getCluster().block()); + System.out.println("managerRx Manager : " + managerRx); + return; + } + + public Mono savePersonErrors(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx); + return personOperationsRx.insertById(Person.class).one(person) // + . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // + .as(transactionalOperator::transactional); + } + + public Mono savePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.insertById(Person.class).one(person) // + .flatMap(Mono::just) // + .as(transactionalOperator::transactional); + } + + public Mono countDuringTx(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return personOperationsRx.save(person) // + .then(personOperationsRx.count(new Query(), Person.class)) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithLogs(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId().toString(), "beforeConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.save(person), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // + .thenMany(personOperationsRx.findByQuery(EventLog.class).all()) // + .as(transactionalOperator::transactional); + } + + public Flux saveWithErrorLogs(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return Flux.merge(personOperationsRx.save(new EventLog(new ObjectId(), "beforeConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterConvert")), // + personOperationsRx.save(new EventLog(new ObjectId(), "beforeInsert")), // + personOperationsRx.save(person), // + personOperationsRx.save(new EventLog(new ObjectId(), "afterInsert"))) // + . flatMap(it -> Mono.error(new RuntimeException("poof!"))) // + .as(transactionalOperator::transactional); + } + + @Transactional + public Flux declarativeSavePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(managerRx, + new DefaultTransactionDefinition()); + + return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person)); + } + + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public Flux declarativeSavePersonErrors(Person person) { + Person p = personOperations.insertById(Person.class).one(person); + // if(1==1)throw new RuntimeException("poof!"); + Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0); + System.err.println("pp=" + pp); + SimulateFailureException.throwEx(); + return Flux.just(p); + } + } + + /* + @Test + public void deletePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(); + })); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxTmplFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .then(rxCBTmpl.removeById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString())) + .then(); + })); + assertThrows(TransactionFailedException.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + // RxRepo //////////////////////////////////////////////////////////////////////////////////////////// + + @Test + public void deletePersonCBTransactionsRxRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()).then(); + })); + result.block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + } + + @Test + public void deletePersonCBTransactionsRxRepoFail() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString()) + .then(rxRepo.withCollection(cName).withTransaction(ctx).deleteById(person.getId().toString())).then(); + })); + assertThrows(TransactionFailedException.class, result::block); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(pFound, person, "Should have found " + person); + } + + @Test + public void findPersonCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive(((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + })); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void insertPersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.insertById(Person.class).inCollection(cName).transaction(ctx).one(person) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailedException e) { + e.printStackTrace(); + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should not have found " + pFound); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + // @Transactional // TODO @Transactional does nothing. Transaction is handled by transactionalOperator + // Failed to retrieve PlatformTransactionManager for @Transactional test: + public void replacePersonRbCBTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(ctx).one(person.getId().toString()) + .flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(ctx) + .one(pFound.withFirstName("Walt"))) + . flatMap(it -> Mono.error(new PoofException())).then(); + }); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof PoofException) { + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + return; + } else { + e.printStackTrace(); + } + } + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void findPersonSpringTransactions() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + List docs = new LinkedList<>(); + Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId())); + Mono result = transactions.reactive((ctx) -> { // get the ctx + return rxCBTmpl.findByQuery(Person.class).inCollection(cName).matching(q).transaction(ctx).one().map(doc -> { + docs.add(doc); + return doc; + }).then(); + }); + result.block(); + assertFalse(docs.isEmpty(), "Should have found " + person); + for (Object o : docs) { + assertEquals(o, person, "Should have found " + person); + } + } + */ + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Bean + public Cluster couchbaseCluster() { + return Cluster.connect("10.144.220.101", "Administrator", "password"); + } + + /* + @Bean("personService") + PersonService getPersonService(CouchbaseOperations ops, CouchbaseTransactionManager mgr, + ReactiveCouchbaseOperations opsRx, ReactiveCouchbaseTransactionManager mgrRx) { + return new PersonService(ops, mgr, opsRx, mgrRx); + } + */ + + } + + @Data + // @AllArgsConstructor + static class EventLog { + public EventLog() {} + + public EventLog(ObjectId oid, String action) { + this.id = oid.toString(); + this.action = action; + } + + public EventLog(String id, String action) { + this.id = id; + this.action = action; + } + + String id; + String action; + @Version + Long version; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java new file mode 100644 index 000000000..1dd8e795d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseReactiveTransactionNativeTests.java @@ -0,0 +1,428 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.transactions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.couchbase.client.java.transactions.TransactionResult; +import com.couchbase.client.java.transactions.error.TransactionFailedException; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.reactive.TransactionalOperator; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbaseReactiveTransactionNativeTests.Config.class) +//@Disabled // Now using TransactionSyncronizationManager for the session +public class CouchbaseReactiveTransactionNativeTests extends JavaIntegrationTests { + + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + @Autowired ReactivePersonRepository rxRepo; + @Autowired PersonRepository repo; + @Autowired ReactiveCouchbaseTemplate rxCBTmpl; + + static String cName; // short name + + static GenericApplicationContext context; + ReactiveCouchbaseTemplate operations; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + cName = null;// cName; + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + if(context != null){ + context.close(); + } + } + + @BeforeEach + public void beforeEachTest() { + operations = rxCBTmpl; + + } + + @Test + public void replacePersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void replacePersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + sleepMs(1000); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + // Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void insertPersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> ctx.template(rxCBTmpl).insertById(Person.class) + .one(person).flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void insertPersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> ctx.template(rxCBTmpl).insertById(Person.class) + .one(person).flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + // Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of "+SimulateFailureException.class); + } + + @Test + public void replacePersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxRepo.withCollection(cName).save(person).block(); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(rxRepo).withCollection(cName).findById(person.getId().toString()) + .flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + // Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void insertPersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(rxRepo).withTransaction(txOperator).withCollection(cName).save(person) // insert + //.flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) // replace + .flatMap(it -> Mono.error(new SimulateFailureException())).then()); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + // Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + // assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + @Test + public void insertPersonRepo() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator + .reactive((ctx) -> ctx.repository(rxRepo).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(rxRepo).withCollection(cName).save(p.withFirstName("Walt"))) // replace + // .flatMap(it -> Mono.error(new PoofException())) + .then()); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof SimulateFailureException) { + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertNull(pFound, "Should NOT have found " + pFound); + return; + } + e.printStackTrace(); + } + Person pFound = rxRepo.withCollection(cName).findById(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } + + /* + @Test + public void replacePersonRbSpringTransactional() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + + + TransactionalOperator txOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> { + ctx.transactionResultHolder(123); + return ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + .as(txOperator::transactional).then(); + }, false); + + //TransactionalOperator txOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager, new CouchbaseTransactionDefinition()); + //Mono result = txOperator.reactive((ctx) -> { + // ctx.transactionResultHolder(123); + // return ctx.template(rxCBTmpl).findById(Person.class).one(person.getId().toString()) + // .flatMap(p -> ctx.template(rxCBTmpl).replaceById(Person.class).one(p.withFirstName("Walt"))) + // .as(txOperator::transactional).then(); + //}, false); + + try { + result.block(); + } catch (TransactionFailedException e) { + if (e.getCause() instanceof PoofException) { + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found " + person); + return; + } + e.printStackTrace(); + } + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + // throw new RuntimeException("Should have been a TransactionFailedException exception with a cause of PoofException"); + } +*/ + @Test + public void findReplacePersonCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block(); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive(ctx -> { + rxCBTmpl.support().getTxResultHolder(person); + return rxCBTmpl.findById(Person.class).inCollection(cName).transaction(txOperator).one(person.getId().toString()) + .flatMap(pGet -> rxCBTmpl.replaceById(Person.class).inCollection(cName).transaction(txOperator) + .one(pGet.withFirstName("Walt"))) + .then(); + }); + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt"); + } + + @Test + public void insertReplacePersonsCBTransactionsRxTmpl() { + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + Mono result = txOperator.reactive((ctx) -> { + return rxCBTmpl + .insertById(Person.class).inCollection(cName).transaction(txOperator).one(person).flatMap(pInsert -> rxCBTmpl + .replaceById(Person.class).inCollection(cName).transaction(txOperator).one(pInsert.withFirstName("Walt"))) + .then(); + }); + + result.block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt"); + } + + @Test void transactionalSavePerson(){ + Person person = new Person(1, "Walter", "White"); + remove(rxCBTmpl, cName, person.getId().toString()); + savePerson(person).block(); + Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()).block(); + assertEquals(person, pFound, "Should have found "+person); + } + + public Mono savePerson(Person person) { + + TransactionalOperator transactionalOperator = TransactionalOperator.create(reactiveCouchbaseTransactionManager); + return operations.save(person) // + .flatMap(Mono::just) // + .as(transactionalOperator::transactional); + } + + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))).block(); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java new file mode 100644 index 000000000..72d91e07c --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTemplateTransactionIntegrationTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.transactions; + +import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.example.demo.CouchbaseTransactionalTemplate; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.data.domain.Persistable; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +import com.couchbase.client.core.cnc.Event; +//import com.example.demo.CouchbaseTransactionManager; +//import com.example.demo.CouchbaseTransactionalTemplate; + +/** + * @author Christoph Strobl + * @currentRead Shadow's Edge - Brent Weeks + */ + +//@ContextConfiguration +@ExtendWith({ SpringExtension.class }) +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@Transactional(transactionManager = BeanNames.COUCHBASE_SIMPLE_CALLBACK_TRANSACTION_MANAGER) +@SpringJUnitConfig(CouchbaseTemplateTransactionIntegrationTests.Config.class) +public class CouchbaseTemplateTransactionIntegrationTests extends JavaIntegrationTests { + + static final String DB_NAME = "template-tx-tests"; + static final String COLLECTION_NAME = "assassins"; + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + @EnableTransactionManagement + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Bean + public CouchbaseTransactionManager transactionManager(@Autowired CouchbaseClientFactory template) { + return new CouchbaseTransactionManager(template, null); + } + + //@Bean + //public CouchbaseTransactionalTemplate transactionalTemplate(CouchbaseTransactionManager manager) { + // return manager.template(); + //} + + } + + @Autowired CouchbaseTemplate template; + + List>> assertionList; + + @BeforeEach + public void setUp() { + assertionList = new CopyOnWriteArrayList<>(); + } + + @BeforeTransaction + public void beforeTransaction() { + template.removeByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + Collection a = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + } + + @AfterTransaction + public void verifyDbState() { + Collection p = template.findByQuery(Assassin.class).withConsistency(REQUEST_PLUS).all(); + System.out.println("assassins: " + p); + assertionList.forEach(it -> { + + boolean isPresent = template.findById(Assassin.class).one(it.getId().toString()) != null; // (Filters.eq("_id", + // it.getId())) != 0; + + assertThat(isPresent).isEqualTo(it.shouldBePresent()) + .withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(), + it.shouldBePresent() ? "be present" : "NOT be present")); + }); + } + + @Test + @Rollback(false) + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public void shouldOperateCommitCorrectly() { + assert(TestTransaction.isActive()); + Assassin hu = new Assassin("hu", "Hu Gibbet"); + template.insertById(Assassin.class).one(hu); + assertAfterTransaction(hu).isPresent(); + } + + @Test + @Disabled // JCBC-1951 - run it twice second time fails. Recreate bucket, it succeeds + @Rollback(true) + @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER) + public void shouldOperateRollbackCorrectly() { + assert(TestTransaction.isActive()); + Assassin vi = new Assassin("vi", "Viridiana Sovari"); + try { + template.removeById(Assassin.class).one(vi.getId()); // could be something that is not an Assassin + } catch (DataRetrievalFailureException dnfe) {} + template.insertById(Assassin.class).one(vi); + assertAfterTransaction(vi).isNotPresent(); + } + + @Test + @Transactional(BeanNames.COUCHBASE_TRANSACTION_MANAGER) + @Disabled // JCBC-1951 - run it twice, second time fails. Recreate bucket, it succeeds + @Rollback(true) + public void shouldBeAbleToViewChangesDuringTransaction(){ + assert(TestTransaction.isActive()); + Assassin durzo = new Assassin("durzo", "Durzo Blint"); + template.insertById(Assassin.class).one(durzo); + Assassin retrieved = template.findById(Assassin.class).one(durzo.getId()); + assertThat(retrieved).isEqualTo(durzo); + assertAfterTransaction(durzo).isNotPresent(); + } + + // --- Just some helpers and tests entities + + private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) { + assertInAnnotationTransaction(false); + AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(assassin); + assertionList.add(assertion); + return assertion; + } + + @Data + @AllArgsConstructor + @Document + static class Assassin implements Persistable { + + @Id String id; + String name; + + @Override + public boolean isNew() { + return id == null; + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java new file mode 100644 index 000000000..640b0c8c2 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionNativeTests.java @@ -0,0 +1,266 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.transactions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Optional; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.ReactivePersonRepository; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator; +import org.springframework.data.couchbase.transaction.ReactiveCouchbaseTransactionManager; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.transactions.error.TransactionFailedException; + +/** + * Tests for com.couchbase.transactions without using the spring data transactions framework + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig(CouchbaseTransactionNativeTests.Config.class) +public class CouchbaseTransactionNativeTests extends JavaIntegrationTests { + + // @Autowired not supported on static fields. These are initialized in beforeAll() + // Also - @Autowired doesn't work here on couchbaseClientFactory even when it is not static, not sure why - oh, it + // seems there is not a ReactiveCouchbaseClientFactory bean + @Autowired CouchbaseClientFactory couchbaseClientFactory; + @Autowired CouchbaseTransactionManager couchbaseTransactionManager; + + @Autowired ReactiveCouchbaseTransactionManager reactiveCouchbaseTransactionManager; + + @Autowired PersonRepository repo; + @Autowired ReactivePersonRepository repoRx; + @Autowired CouchbaseTemplate cbTmpl;; + static String cName; // short name + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + // short names + cName = null;// cName; + } + + @AfterAll + public static void afterAll() { + callSuperAfterAll(new Object() {}); + } + + @Test + public void replacePersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + + assertThrowsWithCause(() -> txOperator.run((ctx) -> { + Person p = txOperator.template(cbTmpl.reactive()).findById(Person.class).one(person.getId().toString()).block(); + Person pp = txOperator.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt")).block(); + throw new PoofException(); + }), TransactionFailedException.class, PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walter", pFound.getFirstname(), "firstname should be Walter"); + + } + + @Test + public void replacePersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause(() -> txOperator.run((ctx) -> { + Person p = ctx.template(cbTmpl.reactive()).findById(Person.class).one(person.getId().toString()).block(); + Person pp = ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt")).block(); + throw new PoofException(); + }), TransactionFailedException.class, PoofException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walter", pFound.getFirstname(), "firstname should be Walter"); + + } + + @Test + public void insertPersonTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + txOperator.reactive((ctx) -> ctx.template(cbTmpl.reactive()).insertById(Person.class).one(person) + .flatMap(p -> ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt"))).then()) + .block(); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt"); + } + + @Test + public void insertPersonRbTemplate() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause( + () -> txOperator.reactive((ctx) -> ctx.template(cbTmpl.reactive()).insertById(Person.class).one(person) + .flatMap(p -> ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new PoofException()))).block(), + TransactionFailedException.class, PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void replacePersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + repo.withCollection(cName).save(person); + + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause(() -> txOperator.run(ctx -> { + ctx.repository(repoRx).withCollection(cName).findById(person.getId().toString()) + .flatMap(p -> ctx.repository(repoRx).withCollection(cName).save(p.withFirstName("Walt"))); + throw new PoofException(); + }), TransactionFailedException.class, PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals(person, pFound, "Should have found " + person); + } + + @Test + public void insertPersonRbRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause(() -> txOperator.reactive((ctx) -> ctx.repository(repoRx).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(repoRx).withCollection(cName).save(p.withFirstName("Walt"))) // replace + .flatMap(it -> Mono.error(new PoofException()))).block(), TransactionFailedException.class, + PoofException.class); + + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertNull(pFound, "Should NOT have found " + pFound); + } + + @Test + public void insertPersonRepo() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + txOperator.reactive((ctx) -> ctx.repository(repoRx).withCollection(cName).save(person) // insert + .flatMap(p -> ctx.repository(repoRx).withCollection(cName).save(p.withFirstName("Walt"))) // replace + .then()).block(); + Optional pFound = repo.withCollection(cName).findById(person.getId().toString()); + assertEquals("Walt", pFound.get().getFirstname(), "firstname should be Walt"); + } + + @Test + public void replacePersonRbSpringTransactional() { + Person person = new Person(1, "Walter", "White"); + remove(cbTmpl, cName, person.getId().toString()); + cbTmpl.insertById(Person.class).inCollection(cName).one(person); + CouchbaseTransactionalOperator txOperator = new CouchbaseTransactionalOperator(reactiveCouchbaseTransactionManager); + assertThrowsWithCause( + () -> txOperator + .reactive((ctx) -> ctx.template(cbTmpl.reactive()).findById(Person.class).one(person.getId().toString()) + .flatMap(p -> ctx.template(cbTmpl.reactive()).replaceById(Person.class).one(p.withFirstName("Walt"))) + .flatMap(it -> Mono.error(new PoofException()))) + .block(), + TransactionFailedException.class, PoofException.class); + Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString()); + assertEquals("Walter", pFound.getFirstname(), "firstname should be Walter"); + } + + void remove(Collection col, String id) { + remove(col.reactive(), id); + } + + void remove(ReactiveCollection col, String id) { + try { + col.remove(id, RemoveOptions.removeOptions().timeout(Duration.ofSeconds(10))); + } catch (DocumentNotFoundException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + void remove(CouchbaseTemplate template, String collection, String id) { + remove(template.reactive(), collection, id); + } + + void remove(ReactiveCouchbaseTemplate template, String collection, String id) { + try { + template.removeById(Person.class).inCollection(collection).one(id).block(); + System.out.println("removed " + id); + } catch (DocumentNotFoundException | DataRetrievalFailureException nfe) { + System.out.println(id + " : " + "DocumentNotFound when deleting"); + } + } + + static class PoofException extends RuntimeException {}; + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/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/transactions/ObjectId.java b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java new file mode 100644 index 000000000..7588e0c89 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/ObjectId.java @@ -0,0 +1,10 @@ +package org.springframework.data.couchbase.transactions; + +import java.util.UUID; + +public class ObjectId{ + public ObjectId(){ + id = UUID.randomUUID().toString(); + } + String id; +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java new file mode 100644 index 000000000..44c68e929 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/SimulateFailureException.java @@ -0,0 +1,15 @@ +package org.springframework.data.couchbase.transactions; + +class SimulateFailureException extends RuntimeException { + + public SimulateFailureException(String... s){ + super(s!= null && s.length > 0 ? s[0] : null); + } + + public SimulateFailureException(){} + + public static void throwEx(String... s){ + throw new SimulateFailureException(s); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/TransactionInterception.save b/src/test/java/org/springframework/data/couchbase/transactions/TransactionInterception.save new file mode 100644 index 000000000..ca64011c3 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/transactions/TransactionInterception.save @@ -0,0 +1,51 @@ +package org.springframework.data.couchbase.transactions; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.data.couchbase.transaction.CouchbaseTransactionManager; +import org.springframework.data.couchbase.transaction.interceptor.CouchbaseTransactionInterceptor; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.config.TransactionManagementConfigUtils; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; + +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +class TransactionInterception { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource, + CouchbaseTransactionManager txManager) { + TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(); + interceptor.setTransactionAttributeSource(transactionAttributeSource); + if (txManager != null) { + interceptor.setTransactionManager(txManager); + } + return interceptor; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public TransactionAttributeSource transactionAttributeSource() { + return new AnnotationTransactionAttributeSource(); + } + + @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor( + TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) { + + BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); + advisor.setTransactionAttributeSource(transactionAttributeSource); + advisor.setAdvice(transactionInterceptor); + // if (this.enableTx != null) { + // advisor.setOrder(this.enableTx.getNumber("order")); + // } + return advisor; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 6e96c5e7a..5deab4ba9 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -19,12 +19,20 @@ import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.core.service.Service; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig; +import com.couchbase.client.java.transactions.config.TransactionsConfig; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -56,15 +64,20 @@ public abstract class ClusterAwareIntegrationTests { @BeforeAll static void setup(TestClusterConfig config) { testClusterConfig = config; - try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), - authenticator(), bucketName())) { - couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), - CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true)); + // todo gp disabling cleanupLostAttempts to simplify output during development + ClusterEnvironment env = ClusterEnvironment.builder() + .transactionsConfig(TransactionsConfig.cleanupConfig(TransactionsCleanupConfig.cleanupLostAttempts(false))) + .build(); + String connectString = connectionString(); + try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectString, + authenticator(), bucketName(), null, env)) { + couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), CreatePrimaryQueryIndexOptions + .createPrimaryQueryIndexOptions().ignoreIfExists(true).timeout(Duration.ofSeconds(300))); // this is for the N1qlJoin test List fieldList = new ArrayList<>(); fieldList.add("parentId"); couchbaseClientFactory.getCluster().queryIndexes().createIndex(bucketName(), "parent_idx", fieldList, - CreateQueryIndexOptions.createQueryIndexOptions().ignoreIfExists(true)); + CreateQueryIndexOptions.createQueryIndexOptions().ignoreIfExists(true).timeout(Duration.ofSeconds(300))); // .with("_class", "org.springframework.data.couchbase.domain.Address")); } catch (IndexFailureException ife) { LOGGER.warn("IndexFailureException occurred - ignoring: ", ife); @@ -122,8 +135,10 @@ public static String connectionString() { } protected static Set seedNodes() { - return config().nodes().stream().map(cfg -> SeedNode.create(cfg.hostname(), - Optional.ofNullable(cfg.ports().get(Services.KV)), Optional.ofNullable(cfg.ports().get(Services.MANAGER)))) + return config().nodes().stream() + .map(cfg -> SeedNode.create(cfg.hostname(), + Optional.ofNullable(cfg.ports().get(Services.KV)), + Optional.ofNullable(cfg.ports().get(Services.MANAGER)))) .collect(Collectors.toSet()); } @@ -178,16 +193,13 @@ private static void callSuper(Object createdHere, Class annotationClass) { for (Method m : methods) { annotation = m.getAnnotation(annotationClass); if (annotation != null) { - if (annotation != null) { - m.invoke(null); - invokedSuper = m; - } + m.invoke(null); + invokedSuper = m; } } if (invokedSuper != null) { // called method is responsible for calling any super methods return; } - } } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java index ffeeb6b5d..bbb4c3fa3 100644 --- a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java @@ -77,7 +77,7 @@ public static void beforeAll() { List fieldList = new ArrayList<>(); fieldList.add("parentId"); - cluster.query("CREATE INDEX `parent_idx` ON default:" + bucketName() + "." + scopeName + "." + collectionName2 + cluster.query("CREATE INDEX `parent_idx` ON default:`" + bucketName() + "`." + scopeName + "." + collectionName2 + "(parentId)"); } catch (IndexExistsException ife) { LOGGER.warn("IndexFailureException occurred - ignoring: ", ife.toString()); diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java index 90b9ab0c8..e9149bf92 100644 --- a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -47,6 +47,9 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.junit.platform.commons.util.UnrecoverableExceptions; +import org.opentest4j.AssertionFailedError; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -111,9 +114,9 @@ public static void beforeAll() { } catch (IOException ioe) { throw new RuntimeException(ioe); } - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + //ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + //couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + //reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); } /** @@ -143,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); @@ -206,9 +209,8 @@ protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, fin } if (!ready) { - createAndDeleteBucket();// need to do this because of https://issues.couchbase.com/browse/MB-50132 try { - Thread.sleep(50); + Thread.sleep(100); } catch (InterruptedException e) {} } } @@ -218,32 +220,6 @@ protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, fin } } - private static void createAndDeleteBucket() { - final OkHttpClient httpClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build(); - String hostPort = connectionString().replace("11210", "8091"); - String bucketname = UUID.randomUUID().toString(); - try { - - Response postResponse = httpClient.newCall(new Request.Builder() - .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url("http://" + hostPort + "/pools/default/buckets/") - .post(new FormBody.Builder().add("name", bucketname).add("bucketType", "membase").add("ramQuotaMB", "100") - .add("replicaNumber", Integer.toString(0)).add("flushEnabled", "1").build()) - .build()).execute(); - - if (postResponse.code() != 202) { - throw new IOException("Could not create bucket: " + postResponse + ", Reason: " + postResponse.body().string()); - } - Response deleteResponse = httpClient.newCall(new Request.Builder() - .header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword())) - .url("http://" + hostPort + "/pools/default/buckets/" + bucketname).delete().build()).execute(); - System.out.println("deleteResponse: " + deleteResponse); - } catch (IOException ioe) { - ioe.printStackTrace(); - } - } - /** * Improve test stability by waiting for a given service to report itself ready. */ @@ -302,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); @@ -327,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 -> { @@ -366,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); @@ -403,4 +379,47 @@ public static void sleepMs(long ms) { Thread.sleep(ms); } catch (InterruptedException ie) {} } + + public static Throwable assertThrowsOneOf(Executable executable, Class... expectedTypes) { + + try { + executable.execute(); + } + catch (Throwable actualException) { + for(Class expectedType:expectedTypes){ + if(actualException.getClass().isAssignableFrom( expectedType)){ + return actualException; + } + } + UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + String message = "Unexpected exception type thrown "+actualException.getClass(); + throw new AssertionFailedError(message, actualException); + } + + String message ="Expected "+expectedTypes+" to be thrown, but nothing was thrown."; + throw new AssertionFailedError(message); + } + + public static Throwable assertThrowsWithCause(Executable executable, Class... expectedTypes) { + Class nextExpectedException= null; + try { + executable.execute(); + } + catch (Throwable actualException) { + for(Class expectedType:expectedTypes){ + nextExpectedException = expectedType; + if(actualException == null || !expectedType.isAssignableFrom( actualException.getClass())){ + String message ="Expected "+nextExpectedException+" to be thrown/cause, but found "+actualException; + throw new AssertionFailedError(message, actualException); + } + actualException = actualException.getCause(); + } + //UnrecoverableExceptions.rethrowIfUnrecoverable(actualException); + return actualException; + } + + String message ="Expected "+expectedTypes[0]+" to be thrown, but nothing was thrown."; + throw new AssertionFailedError(message); + } + } diff --git a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java index 776283df9..0ec98b390 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestCluster.java @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.util; -import static java.nio.charset.StandardCharsets.*; +import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.net.URL; @@ -83,7 +83,7 @@ private static Properties loadProperties() { defaults.load(url.openStream()); } } catch (Exception ex) { - throw new RuntimeException("Could not load properties", ex); + throw new RuntimeException("Could not load properties - maybe is pom instead of jar?", ex); } Properties all = new Properties(System.getProperties()); diff --git a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java index bdaac406a..3dc867ac3 100644 --- a/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java +++ b/src/test/java/org/springframework/data/couchbase/util/TestClusterConfig.java @@ -79,6 +79,7 @@ public Optional clusterCert() { * Finds the first node with a given service enabled in the config. *

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

    * * @param service the service to find. * @return a node config if found, empty otherwise. diff --git a/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java index 1eda812a6..d77693645 100644 --- a/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java @@ -60,7 +60,7 @@ ClusterType type() { @Override TestClusterConfig _start() throws Exception { - bucketname = UUID.randomUUID().toString(); + bucketname = "my_bucket"; //UUID.randomUUID().toString(); Response postResponse = httpClient .newCall(new Request.Builder().header("Authorization", Credentials.basic(adminUsername, adminPassword)) diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java index 06710bb90..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; + }); } /** @@ -96,4 +96,25 @@ public static String readResource(final String filename, final Class clazz) { return s.hasNext() ? s.next() : ""; } + /** + * check if we are/are not in an @Transactional transaction + * @param inTransaction + */ + public static void assertInAnnotationTransaction(boolean inTransaction) { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (StackTraceElement ste : stack) { + if (ste.getClassName().startsWith("org.springframework.transaction.interceptor") + || ste.getClassName().startsWith("org.springframework.data.couchbase.transaction.interceptor")) { + if (inTransaction) { + return; + } + } + } + if (!inTransaction) { + return; + } + throw new RuntimeException( + "in-annotation-transaction = " + (!inTransaction) + " but expected in-annotation-transaction = " + inTransaction); + } + } diff --git a/src/test/resources/integration.properties b/src/test/resources/integration.properties index f097d05bd..ed138ca00 100644 --- a/src/test/resources/integration.properties +++ b/src/test/resources/integration.properties @@ -2,7 +2,7 @@ # If set to false, it is assumed that the host is managing the cluster and # as a result no containers or anything will be spun up. # Options: containerized, mocked, unmanaged -cluster.type=mocked +cluster.type=unmanaged # Default configs for both cases cluster.adminUsername=Administrator cluster.adminPassword=password @@ -11,5 +11,5 @@ cluster.mocked.numNodes=1 cluster.mocked.numReplicas=1 # Entry point configuration if not managed # value of hostname and ns_server port -cluster.unmanaged.seed=127.0.0.1:8091 +cluster.unmanaged.seed=10.144.220.101:8091 cluster.unmanaged.numReplicas=0 diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 9d05fdc15..6efee2b37 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -27,5 +27,7 @@ + " +