Transaction Support for Spring Data Couchbase.

Closes #1145.

commit e2df8e73777a80ea71c15d77dd51252f6766fa94
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Mon Jun 27 18:29:23 2022 -0700

    Rebased and tidied transaction branch.

    In addition to transaction support, some inconsitencies in
    fluent APIs were remedied.

    Documentation to follow.

    Closes #1145.

commit e20ed0814e770e26ec34634429f3ac7710acd8d3
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Fri Jun 24 17:21:45 2022 -0700

    Rebase with main.

commit edadede7a01997a364c1f5169a36e8bb3e4eee11
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Fri Jun 24 10:45:21 2022 -0700

    Add support for Couchbase Transactions.

    The fluent operations are common for options that are common to operations
     with and without transactions.  Once there is a transaction(ctx), or an
    option specific to without-transactions (such as scanConsistency), the
    interfaces are bifurcated, so that an non-transaction option cannot be
    applied to a transaction operation, and a transaction(ctx) cannot be
    applied where a non-transaction option has already been applied.

    Closes #1145.

    Support for @Transactional for blocking and reactive Transactionmanager.

    Closes 1145.

    Transaction Support.

    Transaction Support.

    Porting to SDK-integrated version of transactions

    The transactions logic exists in the Java SDK as of 3.3.0,
    with a slightly different API.

    This is the first effort at the port, which literally just
    compiles.  It will not run as crucial code has been
    commented and todo-ed.  There is work remaining to figure
    out how to complete the port, as some crucial parts (such
    as ctx.commit() and ctx.rollback()) have been intentionally
    removed.

    Continuing work to get the ExtSDKIntegration port working

    Trying to transition to CallbackPreferring manager.

    Added CouchbaseSimpleCallbackTransactionManager, the simplest
    possible implementation of CallbackPreferringTransactionManager,
    combined with a simpler approach to ThreadLocal storage in
    ReactiveInsertByIdSupport.

    Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now
    passing.

    Adding WIP get-and-replace @Transactional support

    (Not yet working as CAS/version field in
    Person is not populated correctly.)

    Datacouch 1145 transaction support (#1423)

    * Porting to SDK-integrated version of transactions

    The transactions logic exists in the Java SDK as of 3.3.0,
    with a slightly different API.

    This is the first effort at the port, which literally just
    compiles.  It will not run as crucial code has been
    commented and todo-ed.  There is work remaining to figure
    out how to complete the port, as some crucial parts (such
    as ctx.commit() and ctx.rollback()) have been intentionally
    removed.

    * Continuing work to get the ExtSDKIntegration port working

    Trying to transition to CallbackPreferring manager.

    * Added CouchbaseSimpleCallbackTransactionManager, the simplest
    possible implementation of CallbackPreferringTransactionManager,
    combined with a simpler approach to ThreadLocal storage in
    ReactiveInsertByIdSupport.

    Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now
    passing.

    * Adding WIP get-and-replace @Transactional support

    (Not yet working as CAS/version field in
    Person is not populated correctly.)
    Commit before pulling Graham's changes.

    Merge branch 'datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into datacouch_1145_transaction_support

    Transitioning to use CoreTransactionAttemptContext.
    Tests may fail.

    Removing AttemptContextReactiveAccessor

    Don't think we need this, as we can pass around
    CoreTransactionAttemptContext instead, which gives
    access to a lot of internals.

    Removing TransactionsReactive

    Would prefer not to C&P a huge class out of the transaction
    internals, and don't think we need it.

    Removing some files not currently used

    To reduce & simplify the amount of code to look at.

    Some don't seem to be used in any branch, some just
    aren't used in this branch.

    Removing CouchbaseTransactionInterceptor

    As per offline discussion, CallbackPreferringPlatformTransactionManager
    is perhaps the optimal solution.

    Copying @Transactional tests out into separate class

    Tidyup

    Tidyup test names

    Verify GenericSupport is on same thread before and after transactional operation

    Refactoring CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage management

    Using latest java-client

    ReactiveReplaceByIdSupport - Fixing use of CAS now have CoreTransactionAttemptContext.

    Removing unused code.

    ReactiveInsertByIdSupport - fixing use of reactive vs non-reactive, and CAS

    Merging upstream

    Remove incorrect thread check (.doOnNext could execute on a different thread)

    Get scope and collection from pseudoArgs and some cleanup.

    Completing merge from upstream

    Removing unused classes

    Give GenericSupport a better name

    Reject at runtime options that aren't supported in a transaction

    Fixing some small todos, partly by removing unused coe

    Fix runtime option checks

    Simplifying CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage

    Standardising on ReactiveCouchbaseResourceHolder rather than
    holding CoreTransactionAttemptContext too

    Removing version from CouchbaseDocument

    Can't recall why I added this, and tests pass without it

    Improving CouchbaseTransactionalIntegrationTests and adding more tests

    Reject operations that aren't allowed in a transaction (upsertById etc.)

    Improve handling of CAS mismatch

    By calling CoreTransactionAttemptContext.operationFailed,
    it ensures that internal state is set.  So even if
    the user catches the exception, the transaction still behaves
    as it should.

    Removing a now-redundant non-transactional check on upsertById

    I missed this when adding TransactionalSupport.verifyNotInTransaction
    here.

    Support @Transactional options timeout and isolation level

    Add ReactiveTransactionWrapper/TransactionWrapper and a bunch of cleanup.

    Merge branch 'datacouch_1145_transaction_support' of https://github.com/programmatix/spring-data-couchbase into programmatix-datacouch_1145_transaction_support

    Fixed up merge issues.

    Datacouch 1145 transaction support (#1447)

    * Move CouchbaseTransactionalOperator to use SLF4J, same as
    rest of the code.

    * Handle all propagation levels

    * Adding new tests for repository calls inside @Transactional

    One test is failure due to what looks like a bug elsewhere.

    * Rename CouchbaseTransactionalIntegrationTests, and check
    after each test that we're not in a transaction.
    Remove unnecessary methods From ReactiveCouchbaseClientFactory.

    Also rationalized naming of methods and other changes.

    Cleanup of test classes.

    Datacouch 1145 transaction support (#1448)

    * Move CouchbaseTransactionalOperator to use SLF4J, same as
    rest of the code.

    * Handle all propagation levels

    * Adding new tests for repository calls inside @Transactional

    One test is failure due to what looks like a bug elsewhere.

    * Rename CouchbaseTransactionalIntegrationTests, and check
    after each test that we're not in a transaction.

    * Remove unnecessary methods From ReactiveCouchbaseClientFactory.

    Also rationalized naming of methods and other changes.

    * Cleanup of test classes.

    * Removing unused classes

    (Reducing the cognitive burden)

    * Removing version from CouchbaseDocument

    This change was done previously - it must have slipped back in
    a merge.

    * Adding and removing TODOs

    * Adding and removing TODOs

    * DRYing CouchbaseTransactionalPropagationIntegrationTests

    * Check propagation tests retry as expected

    * Tidy up PersonWithoutVersion to the minimum required

    * Removing unused code

    This should all be non-destructive.  Just removing code IntelliJ
    declares unused.  Intent is to make it easier to figure out
    how the CoreTransactionAttemptContext TLS is working.

    * Adding tests for @Transactional removeByQuery and findByQuery

    Failing as they aren't being executed transactionally - investigating why.

    Co-authored-by: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
    change references to resource holder

    change refs to resource holder

    Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support

    Removing core transaction attempt context bound couchbase client factory rebased (#1449)

    * Move ReactiveTransactionsWrapper tests into a new file

    * (Temporarily?) disabling tests using CouchbaseTransactionOperation or TransactionalOperator

    As I feel we should be removing/not-supporting these, and
    on this branch I've broken them.

    * Make all transaction tests call assertNotInTransaction

    * Removing unused code

    * Instead of binding the transaction AttemptContext to a
    CouchbaseClientFactory, fetch it from ThreadLocalStorage
    (or the reactive context) instead.

    This allows a lot of simplifying:
    * The non-trivial ReactiveCouchbaseClientUtils can be removed
    * As can CoreTransactionAttemptContextBoundCouchbaseClientFactory

    Also removing TransactionalSupport.one as it wasn't providing
    as much DRY utility as I thought it would - only used in two
    places.

    This change won't compile on its own.  To reduce the complexity
    of this patchset, the Reactive*OperationSupport changes will
    go into a separate commit.

    * Reactive*OperationSupport changes to support the previous commit.

    * Fixing ReactiveRemoveByQuerySupport.

    Both to support the changes to TransactionalSupport.
    And to fix the TODO where the query resuls were
    not being handled.

    * Disabling a test

    * Adding CouchbaseTransactionsWrapperTemplateIntegrationTests

    * Another advantage of removing CoreTransactionAttemptContextBoundCouchbaseClientFactory
    is we can remove Cluster and ClusterInterface.

    * Adding CouchbaseReactiveTransactionsWrapperTemplateIntegrationTests

    Some of these tests are currently failing - tracking down where the
    issue is.
    Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support

    manual merges for PR

    manual merges for PR

    Fix a bunch of test cases and remove unused bits.

    Removing CouchbaseTransactionInterceptor

    As discussed on Slack.

    Reenabling some tests that are passing

    (Unclear why these were disabled?)

    Verified that CallbackPreferringPlatformTransactionManager getTransaction/commit/rollback are never called

    Adding tests for TransactionTemplate, which works fine with CouchbaseSimpleCallbackTransactionManager

    Whether we actually document this support is another matter - it's
    yet another way of doing transactions.

    Small fixes to support TransactionTemplate

    * Handle if the user has set isRollbackOnly on the TransactionStatus
      (which is only available - I think - when using TransactionTemplate)
    * Supply a `transaction` object to CouchbaseTransactionStatus so that
      status.isNewTransaction() correctly returns true.  (This method requires
      that transaction to be set.)

    Clarifying that direct use of PlatformTransactionManager is not supported

    Adding further TransactionTemplateIntegrationTests tests

    Fixing removeByQuery queryOptions creation

    Removing now-fixed TODO (no longer key off Cluster)

    Adding overload to CouchbaseSimpleCallbackTransactionManager to allow
    it to be constructed without a TransactionOptions.

    Adding CouchbaseSimpleTransactionalOperator, the simplest possible
    implementation of TransactionalOperator.

    Adding retry tests

    Just making sure that error handling and retries are done correctly throughout.

    Updating and adding some TODOs

    Have CouchbaseTransactionManager support CouchbaseResourceHolder.class binding

    Adding more tests for CouchbaseTransactionManager.

    These tests fail, and are known to fail.  I'm adding them as a solid demonstration of
    why I don't feel we can have this CouchbaseTransactionManager: it doesn't
    provide the crucial 'core loop' functionality, including error handling
    and retries.  We should standardise on CouchbaseSimpleCallbackTransactionManager
    instead.

    CouchbaseSimpleCallbackTransactionManager.executeNewReactiveTransaction now
    buffers results rather than trying to stream a Flux from out of a
    completed lambda (which I doubt is even possible.)

    Removing comment that has been resolved.

    (As per Slack, we will live with this limitation.)

    Adding CouchbaseSimpleTransactionInterceptor, a very
    simple TransactionInterceptor implemention that
    defers to CouchbaseSimpleCallbackTransactionManager
    if that is the provided TransactionManager, and otherwise
    just calls super.

    This allows reactive @Transactional - though all
    @Transactional methods including blocking will now flow
    through it.

    There are two rather divergent approaches in the code currently:

    1. CouchbaseTransactionManager, ReactiveTransactionManager, CouchbaseTransactionalOperator, CouchbaseTransactionInterceptor
    2. CouchbaseSimpleCallbackTransactionManager, CouchbaseSimpleTransactionalOperator, CouchbaseSimpleTransactionInterceptor

    I know the intent is to remove some aspects of (1), but until
    that's done it's proving tricky to have tests for both
    concurrently - I've hit several issues on adding
    CouchbaseSimpleTransactionInterceptor, with 'multiple
    transaction manager beans in config' being common.

    So, temporarily moving some beans from
    AbstractCouchbaseConfiguration into the test Config class,
    renaming it, and having two separately TransactionsConfig classes
    for the two approaches.

    Once we've aligned the approaches more, can move what beans
    survive back into AbstractCouchbaseConfiguration.

    Safety check in CouchbaseSimpleCallbackTransactionManager
    that the blocking run is not accidentally
    running a reactive @Transactional somehow.

    Adding tests for reactive @Transactional, which now works
    (including error handling and retries)  as of the
     CouchbaseSimpleTransactionInterceptor.

    With TransactionsConfigCouchbaseSimpleTransactionManager change,
    can now simplify @Transactional(transactionManager = ...) to
    just @Transactional.

    Tidying TODOs

    I saw in a PR comment that getResources no longer uses TransactionOptions

    Removing configureTransactions from config

    As per PR discussion

    Removing some code that has now been refactored into 3.3.1 SDK

    Removing TODOs that are TODONE already

    Removing transactionsOptions() bean from AbstractCouchbaseConfiguration.

    As per comment, this feels unnecessary: any options you'd configure at
    this config level, you'd surely provide at the global (Cluster) level
    instead?

    Reinstating some commented-out code

    This looks pretty crucial - can't recall why I commented it in first place

    Removing TransactionResult

    This was from a now-abandoned idea of storing transactional
    metadata in the entity class.

    Fix recent removal of TransactionOptions bean

    Switch some IllegalStateException for more accurate UnsupportedOperationException

    Tidying tests to remove old GenericApplicationContext method

    Tidying some TODOs

    (todo gp == in code that I think we should remove)
    (todo gpx == needs looking at)

    Tidying TransactionsConfigCouchbaseSimpleTransactionManager

    Provide SpringTransactionAttemptContext and ReactiveSpringTransactionAttemptContext wrappers

    For use by TransactionsWrapper and reactive equivalent.

    Pro: it's an abstraction layer.  It lets us add Spring-specific functionality, or hide
        functionality that for whatever reason doesn't work with Spring
        (which is why it's composition rather than inheritance, beyond that being
        a best practice anyway).

    Con: any new API added will also have to be added to these wrappers.  But
        that's a small amount of work and API is added very infrequently.

    Cleanup tests mostly.  Temporary fix for CouchbaseSimpleCallbackTransactionManager.

    Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support

    Tidying up test cases.

    Fixes to over-zealous manual merging.

    - keep CouchbaseTransactionManager as Graham is still using it.
    - fix tests that were expecting SimulateFailure to be nested.

    Add ReactiveTransactionWrapper and enable tests that use it.

    More tidying. Removed CouchbaseTransactionManager.

    Only wrap exceptions in CouchbaseSimpleTransactionInterceptor if they are not RuntimeExceptions.

    Removed CouchbaseTransactionalOperator and ReactiveCouchbaseTransactionalOperator.

    Fixed non-transactions regression introduced in previous commit.

    Removing the two transactions config classes

    Now we've landed on a single agreed approach there's no
    need for this separation any more, and any beans can be
    moved back into AbstractCouchbaseConfiguration.

    Rename CouchbaseSimpleTransactionInterceptor

    The "Simple" moniker is no longer useful since we now
    only have one of these.

    Rename CouchbaseSimpleCallbackTransactionManager

    The "Simple" moniker is no longer useful since we now
    only have one of these.

    Rename CouchbaseSimpleTransactionalOperator

    The "Simple" moniker is no longer useful since we now
    only have one of these.

    Change CouchbaseTransactionalOperator construction to static

    As per Slack discussion.

    Remove comments related to another database

    Removing a test comment that doesn't seem to apply anymore

    Test passes for me at least

    Simplify TransactionsWrapper and AttemptContextReactiveAccessor

    newCoreTranactionAttemptContext is reimplementing some code
    from core plus has some config bugs to resolve.  I think it's
    simpler to remove it and replace TransactionsWrapper
    (the only code still using this method) with the simple code
    seen now.

    (Note this is similar to how I had it before
    1701183b63
    - not sure if that commit intentionally reverted things?)

    This change also petmits a lot of tidyup & simplification throughout
    the codebase.  We can just create CouchbaseResourceHolders
    directly now.

    Tidying a test

    No longer needs retryWhen now using the new approaches

    Remove ReactiveCouchbaseClientFactory.

    It was added to support getting the transaction from the
    TransactionSynchronizationManager.forCurrent() and providing a template
    with a session containing the transaction.

    Fixing some code warnings

    Mostly removing unused code

    Mark internal classes @Stability.Internal

    Removing some tests that have already been previously moved into another file.

    Removing the transaction wrappers

    Requires JVMCBC-1105 and 3.3.2

    Add more tests for native SDK transactions

    Move all tests related to native SDK transactions into their own package

    It makes it easier to test just that functionality
    while iterating.

    Removing CouchbaseTransactionManagerTransactionalTemplateIntegrationTests

    This test is now redundant.  It was created to show
    why the original CouchbaseTransactionManager couldn't work
    (no retries).  Now that has been replaced, this test
    is just duplicating others.

    Adding some minimal JavaDocs and comments.

    Tidying up after moving ThreadLocalStorage into SDK

    Deleting CouchbaseTemplateTransactionIntegrationTests

    As this relies on Spring test @Transactional, which
    we do not support as that Spring logic is not aware
    of CallbackPreferringTransactionManager.

    Removed some redundant bean names

    Tidying up tests and comments

    Removing some now-unused reflection code

    Starting with mapping TransactionFailedException and
    TransactionCommitAmbiguousException, into new errors
    TransactionSystemUnambiguousException and
    TransactionSystemAmbiguousException.

    These will be raised from an @Transactional
    transaction.

    E.g. to do error handling the user would do:

    ```
    try {
       service.transactionalMethod();
    }
    catch (TransactionSystemAmbiguousException ex) {
      // app-specific handling
    }
    catch (TransactionSystemUnambiguousException ex) {
      // app-specific handling
    }

    class Service {
        @Transactional
        void transactionalMethod() {
          // ...
        }
    }
    ```

    Mapping TransactionOperationFailedException, which is
    an opaque signal raised from transaction operations,
    to new exception UncategorizedTransactionDataAccessException.

    This depends on some new functionality added into
    Java SDK 3.3.2, WrappedTransactionOperationFailedException.

    Minor tidyuo

    Improving tests for correct operation-level errors

commit b02959dded4cf19b2f8b6f78b016b5bb6afb9ac8
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Mon Mar 14 09:37:51 2022 -0700

    Support derived queries on repositories defined with an abstract entity class. (#1366)

    Motivation:
    Currently an abstract entity class specified in the repository definition can
    be used for the predicate typeKey = typeAlias(of abstract entity class) in
    queries. Since documents are stored with typeKey = typeAlias(of concrete class)
    those queries will never match any documents.  To allow this to work,
    all of the abstract entity class an all concrete subclasses must use the
    same typeAlias.

    Once those documents are found, regardless of their concrete class,
    they will all have the same typeKey = typeAlias, instead of having the
    typeAlias specific to the concrete class. Additional information in
    the stored document is needed to identify the concrete class (subtype
    in the example test case), as well as a TypeMapper to interpret that
     information.

    Changes:
    This allows a common TypeAlias to be used for the purpose of the predicate
    typeKey = typeAlias, and the determination of the concrete type by
    implementing an AbstractingMappingCouchbaseConverter that inspects the
    'subtype' property.

    Closes #1365.

    Co-authored-by: Michael Reiche <michael.reiche@couchbase.com>

commit 5f03d1e0ce80e820ff8a3322b2ebfcf6f1bebb8f
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Tue Feb 22 10:09:25 2022 -0800

    Fix update to cache documentation. (#1359)

    Closes #1358.

commit d5ca390d84a281b31854aa201cc53b4093af075d
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Tue Feb 22 09:56:53 2022 -0800

    Reinstate CouchbaseCache documentation. (#1357)

    Closes #1356.

commit 9af8ece3708d805c03db8784bfd9cd3c77e76db9
Author: Mark Paluch <mpaluch@vmware.com>
Date:   Tue Feb 22 08:44:34 2022 +0100

    Use Java 17 to build snapshots for Artifactory.

    Closes #1352

commit e83bd41a86528ceb99d2291a23ac07fe6250b75f
Author: Mark Paluch <mpaluch@vmware.com>
Date:   Fri Feb 18 11:15:41 2022 +0100

    After release cleanups.

    See #1307

commit 13d266e6f7ff06901c9359ef7aac5143858d22f1
Author: Mark Paluch <mpaluch@vmware.com>
Date:   Fri Feb 18 11:08:48 2022 +0100

    Prepare 4.4 M3 (2021.2.0).

    See #1307

commit 4a044197aa5d7c4bfc5d9b15f0dc7139bb4b4fc5
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Mon Feb 14 09:10:04 2022 -0800

    Add mechanism for save to do one of insert, replace or upsert. (#1316)

    Closes #1277.

commit 1fdbf6d5d091225d3297b3aab892013155000e06
Author: Christoph Strobl <cstrobl@vmware.com>
Date:   Tue Jan 18 09:21:34 2022 +0100

    After release cleanups.

    See #1301

commit da7d241e3927412b5ce32b9a1d017a2412c1891e
Author: Christoph Strobl <cstrobl@vmware.com>
Date:   Tue Jan 18 09:09:02 2022 +0100

    Prepare 4.4 M2 (2021.2.0).

    See #1301

commit ebe33cf138970ad47575452bd8b1fc181964fba7
Author: Christoph Strobl <cstrobl@vmware.com>
Date:   Fri Jan 14 11:08:04 2022 +0100

    After release cleanups.

    See #1298

commit ecadff644c31a834d83c2061cb45edb43f2e8983
Author: Christoph Strobl <cstrobl@vmware.com>
Date:   Fri Jan 14 10:57:51 2022 +0100

    Prepare 4.4 M1 (2021.2.0).

    See #1298

commit 5dbf182fd10c5405b7a9361f50188144b76239b8
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Thu Jan 6 15:00:49 2022 -0800

    Incorporate changes from 5.0.x and bump Couchbase SDK. (#1287)

    Closes #1286.

    Co-authored-by: mikereiche <michael.reiche@couchbase.com>

commit ef22b3d7967f805d74ddb92e571899570cd80762
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date:   Fri Nov 12 07:15:45 2021 -0800

    Handle Collection<> parameters to repository query methods. (#1271)

    Closes #1270.

    Co-authored-by: mikereiche <michael.reiche@couchbase.com>

commit 00e05e4b41414afd815cf08127da13c2bc3481d1
Author: Jens Schauder <jschauder@vmware.com>
Date:   Fri Nov 12 10:59:45 2021 +0100

    After release cleanups.

    See #1257

commit af66ada42cd19ce8d6ac989188686155d3a8181a
Author: Jens Schauder <jschauder@vmware.com>
Date:   Fri Nov 12 10:49:16 2021 +0100

    Prepare 4.3 GA (2021.1.0).

    See #1257

commit 6ec138182da008c7be465fb5e31f0e210e3b00ca
Author: yyfMichaelYan <54760415+yyfMichaelYan@users.noreply.github.com>
Date:   Mon Nov 8 13:40:24 2021 -0600

    fix flaky test writesAndReadsCustomFieldsConvertedClass (#1264)
This commit is contained in:
Michael Reiche
2022-06-30 12:14:28 -07:00
parent 62feffa89d
commit c5efd9a89e
103 changed files with 7879 additions and 891 deletions

View File

@@ -16,6 +16,12 @@
package org.springframework.data.couchbase.cache;
import static org.junit.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.couchbase.util.Capabilities;
@@ -23,12 +29,6 @@ import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests;
import org.springframework.data.couchbase.util.IgnoreWhen;
import java.util.UUID;
import static org.junit.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* CouchbaseCache tests Theses tests rely on a cb server running.
*

View File

@@ -34,8 +34,7 @@ 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 com.couchbase.client.java.query.QueryOptions;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* CouchbaseCache tests Theses tests rely on a cb server running.
@@ -43,6 +42,7 @@ import com.couchbase.client.java.query.QueryOptions;
* @author Michael Reiche
*/
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(Config.class)
class CouchbaseCacheIntegrationTests extends JavaIntegrationTests {
volatile CouchbaseCache cache;
@@ -68,8 +68,6 @@ class CouchbaseCacheIntegrationTests extends JavaIntegrationTests {
super.afterEach();
}
@Test
void cachePutGet() {
CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1");
@@ -125,7 +123,6 @@ class CouchbaseCacheIntegrationTests extends JavaIntegrationTests {
assertEquals(user1, cache.get(user1.getId()).get()); // user1.getId() is still user1
}
@Test // this WORKS
public void clearWithDelayOk() throws InterruptedException {
cache.put("KEY", "VALUE");

View File

@@ -37,7 +37,7 @@ import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.couchbase.core.ExecutableFindByIdOperation.ExecutableFindById;
@@ -48,6 +48,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllId;
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;
@@ -59,6 +60,7 @@ import org.springframework.data.couchbase.domain.UserSubmission;
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.CouchbaseException;
import com.couchbase.client.java.kv.PersistTo;
@@ -66,8 +68,6 @@ import com.couchbase.client.java.kv.ReplicateTo;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency;
;
/**
* KV tests Theses tests rely on a cb server running.
*
@@ -75,12 +75,15 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @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();
@@ -100,7 +103,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
User foundUser = couchbaseTemplate.findById(User.class).withExpiry(Duration.ofSeconds(1)).one(user1.getId());
user1.setVersion(foundUser.getVersion());// version will have changed
assertEquals(user1, foundUser);
sleepMs(2000);
sleepMs(3000);
Collection<User> foundUsers = (Collection<User>) couchbaseTemplate.findById(User.class)
.all(Arrays.asList(user1.getId(), user2.getId()));

View File

@@ -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;
@@ -36,11 +37,13 @@ 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;
@@ -54,6 +57,7 @@ import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.error.AmbiguousTimeoutException;
import com.couchbase.client.core.error.UnambiguousTimeoutException;
@@ -67,7 +71,6 @@ import com.couchbase.client.java.kv.RemoveOptions;
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
@@ -77,8 +80,12 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @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
@@ -103,17 +110,15 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
// first call the super method
super.beforeEach();
// then do processing for this class
couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName)
.inCollection(collectionName).all();
couchbaseTemplate.findByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName)
.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)
couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope)
.inCollection(otherCollection).all();
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope)
.inCollection(otherCollection).all();
}
@@ -123,8 +128,7 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
// 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).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
// then call the super method
super.afterEach();
}
@@ -137,8 +141,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS)
.inCollection(collectionName).all();
for (User u : foundUsers) {
if (!(u.equals(user1) || u.equals(user2))) {
@@ -180,8 +184,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser));
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS)
.inCollection(collectionName).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
}
@@ -205,8 +209,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
Query daveUsers = new Query(QueryCriteria.where("username").like("dave"));
final List<UserSubmissionProjected> foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class)
.as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inCollection(collectionName).matching(daveUsers).all();
.as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName)
.matching(daveUsers).all();
assertEquals(1, foundUserSubmissions.size());
assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername());
assertEquals(user.getId(), foundUserSubmissions.get(0).getId());
@@ -223,16 +227,16 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<UserJustLastName> foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
.withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
final List<UserJustLastName> foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class)
.as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName)
.matching(specialUsers).all().collectList().block();
.as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers)
.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();
}
@@ -247,8 +251,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId()));
assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId()));
List<RemoveResult> result = couchbaseTemplate.removeByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
List<RemoveResult> result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS)
.inCollection(collectionName).all();
assertEquals(2, result.size(), "should have deleted user1 and user2");
assertNull(
@@ -272,8 +276,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
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).withConsistency(REQUEST_PLUS).inCollection(collectionName)
.matching(nonSpecialUsers).all();
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId()));
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId()));
@@ -297,17 +301,17 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
// distinct icao
List<Airport> airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
.withConsistency(REQUEST_PLUS).inCollection(collectionName).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();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).count();
assertEquals(7, count1);
// count( distinct (all fields in icaoClass)
@@ -316,7 +320,7 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
String icao;
}).getClass();
long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count();
.withConsistency(REQUEST_PLUS).inCollection(collectionName).count();
assertEquals(7, count2);
} finally {
@@ -341,25 +345,27 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
// distinct icao
List<Airport> airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all()
.collectList().block();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {})
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all()
.collectList().block();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block();
assertEquals(7, airports2.size());
// count( distinct 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(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count()
.block();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).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).distinct(new String[] {}).as(icaoClass)
.withConsistency(REQUEST_PLUS).inCollection(collectionName).count().block();
assertEquals(7, count2);
} finally {
@@ -432,9 +438,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName)
.one(vie.withIcao("441"));
try {
List<Airport> found = couchbaseTemplate.findByQuery(Airport.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName)
.withOptions(options).all();
List<Airport> found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS)
.inScope(scopeName).inCollection(collectionName).withOptions(options).all();
assertEquals(saved.getId(), found.get(0).getId());
} finally {
couchbaseTemplate.removeById().inScope(scopeName).inCollection(collectionName).one(saved.getId());
@@ -485,9 +490,9 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10));
Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(scopeName).inCollection(collectionName)
.one(vie.withIcao("495"));
List<RemoveResult> 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<RemoveResult> removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS)
.inScope(scopeName).inCollection(collectionName).withOptions(options)
.matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all();
assertEquals(saved.getId(), removeResults.get(0).getId());
}
@@ -576,9 +581,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection)
.one(vie.withIcao("594"));
try {
List<Airport> found = couchbaseTemplate.findByQuery(Airport.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection)
.withOptions(options).all();
List<Airport> found = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS)
.inScope(otherScope).inCollection(otherCollection).withOptions(options).all();
assertEquals(saved.getId(), found.get(0).getId());
} finally {
couchbaseTemplate.removeById().inScope(otherScope).inCollection(otherCollection).one(saved.getId());
@@ -629,9 +633,9 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10));
Airport saved = couchbaseTemplate.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection)
.one(vie.withIcao("648"));
List<RemoveResult> 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<RemoveResult> removeResults = couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS)
.inScope(otherScope).inCollection(otherCollection).withOptions(options)
.matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all();
assertEquals(saved.getId(), removeResults.get(0).getId());
}
@@ -694,9 +698,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
@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)
.withConsistency(REQUEST_PLUS).inScope(otherScope).inCollection(otherCollection).withOptions(options).all());
}
@Test
@@ -734,8 +737,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
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)
() -> couchbaseTemplate.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope)
.inCollection(otherCollection).withOptions(options)
.matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all());
}
@@ -760,9 +763,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
try {
UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName)
.one(user);
List<UserCol> found = couchbaseTemplate.findByQuery(UserCol.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName)
.matching(query).all();
List<UserCol> found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS)
.inScope(scopeName).inCollection(collectionName).matching(query).all();
assertEquals(saved, found.get(0), "should have found what was saved");
couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query)
.all();
@@ -781,9 +783,8 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn
try {
UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName)
.one(user);
List<UserCol> found = couchbaseTemplate.findByQuery(UserCol.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName)
.matching(query).all();
List<UserCol> found = couchbaseTemplate.findByQuery(UserCol.class).withConsistency(REQUEST_PLUS)
.inScope(scopeName).inCollection(collectionName).matching(query).all();
assertEquals(saved, found.get(0), "should have found what was saved");
couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query)
.all();

View File

@@ -33,11 +33,13 @@ import java.util.stream.Collectors;
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.couchbase.util.JavaIntegrationTests;
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,12 @@ import org.springframework.data.domain.Sort;
* @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() {
@@ -267,6 +274,15 @@ class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests {
.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 +314,8 @@ class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests {
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);

View File

@@ -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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -37,6 +38,7 @@ import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.OneAndAllEntityReactive;
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;
@@ -55,10 +58,10 @@ import org.springframework.data.couchbase.domain.UserAnnotated3;
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.kv.PersistTo;
import com.couchbase.client.java.kv.ReplicateTo;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* KV tests Theses tests rely on a cb server running.
@@ -67,15 +70,24 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @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() {
super.beforeEach();
List<RemoveResult> r1 = reactiveCouchbaseTemplate.removeByQuery(User.class).all().collectList().block();
List<RemoveResult> r2 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated.class).all().collectList().block();
List<RemoveResult> r3 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated2.class).all().collectList().block();
List<RemoveResult> r1 = reactiveCouchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all()
.collectList().block();
List<RemoveResult> r2 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated.class).withConsistency(REQUEST_PLUS)
.all().collectList().block();
List<RemoveResult> r3 = reactiveCouchbaseTemplate.removeByQuery(UserAnnotated2.class).withConsistency(REQUEST_PLUS)
.all().collectList().block();
List<UserAnnotated2> f3 = reactiveCouchbaseTemplate.findByQuery(UserAnnotated2.class).withConsistency(REQUEST_PLUS)
.all().collectList().block();
}
@Test
@@ -91,15 +103,14 @@ class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationT
.one(user1.getId()).block();
user1.setVersion(foundUser.getVersion());// version will have changed
assertEquals(user1, foundUser);
sleepMs(2000);
sleepMs(3000);
Collection<User> foundUsers = (Collection<User>) reactiveCouchbaseTemplate.findById(User.class)
.all(Arrays.asList(user1.getId(), user2.getId())).collectList().block();
assertEquals(1, foundUsers.size(), "should have found exactly 1 user");
assertEquals(user2, foundUsers.iterator().next());
} finally {
reactiveCouchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all()
.collectList().block();
reactiveCouchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all().collectList().block();
}
}

View File

@@ -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.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.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.Capabilities;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.error.AmbiguousTimeoutException;
import com.couchbase.client.core.error.UnambiguousTimeoutException;
@@ -64,7 +69,6 @@ import com.couchbase.client.java.kv.RemoveOptions;
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,14 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @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 +109,16 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
// first call the super method
super.beforeEach();
// then do processing for this class
couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
couchbaseTemplate.removeByQuery(Airport.class).inScope(scopeName).inCollection(collectionName).all();
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName)
.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)
couchbaseTemplate.removeByQuery(Airport.class).inScope(otherScope).inCollection(otherCollection).all();
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope)
.inCollection(otherCollection).all();
template = reactiveCouchbaseTemplate;
}
@AfterEach
@@ -121,8 +127,7 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
// 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).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
// then call the super method
super.afterEach();
}
@@ -135,8 +140,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS)
.inCollection(collectionName).all();
for (User u : foundUsers) {
if (!(u.equals(user1) || u.equals(user2))) {
@@ -178,8 +183,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser));
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
final List<User> foundUsers = couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS)
.inCollection(collectionName).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
}
@@ -203,8 +208,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
Query daveUsers = new Query(QueryCriteria.where("username").like("dave"));
final List<UserSubmissionProjected> foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class)
.as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inCollection(collectionName).matching(daveUsers).all();
.as(UserSubmissionProjected.class).withConsistency(REQUEST_PLUS).inCollection(collectionName)
.matching(daveUsers).all();
assertEquals(1, foundUserSubmissions.size());
assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername());
assertEquals(user.getId(), foundUserSubmissions.get(0).getId());
@@ -221,12 +226,12 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
Query specialUsers = new Query(QueryCriteria.where("firstname").like("special"));
final List<UserJustLastName> foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
.withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all();
assertEquals(1, foundUsers.size());
final List<UserJustLastName> foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class)
.as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName)
.matching(specialUsers).all().collectList().block();
.as(UserJustLastName.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).matching(specialUsers)
.all().collectList().block();
assertEquals(1, foundUsersReactive.size());
}
@@ -242,8 +247,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user1.getId()));
assertTrue(couchbaseTemplate.existsById().inScope(scopeName).inCollection(collectionName).one(user2.getId()));
List<RemoveResult> result = couchbaseTemplate.removeByQuery(User.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
List<RemoveResult> result = couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS)
.inCollection(collectionName).all();
assertEquals(2, result.size(), "should have deleted user1 and user2");
assertNull(
@@ -267,8 +272,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
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).withConsistency(REQUEST_PLUS).inCollection(collectionName)
.matching(nonSpecialUsers).all();
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId()));
assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId()));
@@ -292,17 +297,17 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
// distinct icao
List<Airport> airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all();
.withConsistency(REQUEST_PLUS).inCollection(collectionName).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();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).count();
assertEquals(7, count1);
// count( distinct (all fields in icaoClass)
@@ -311,7 +316,7 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
String icao;
}).getClass();
long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count();
.withConsistency(REQUEST_PLUS).inCollection(collectionName).count();
assertEquals(7, count2);
} finally {
@@ -336,25 +341,23 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
// distinct icao
List<Airport> airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" })
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all()
.collectList().block();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block();
assertEquals(2, airports1.size());
// distinct all-fields-in-Airport.class
List<Airport> airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {})
.as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all()
.collectList().block();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all().collectList().block();
assertEquals(7, airports2.size());
// count( distinct 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(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count()
.block();
.as(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).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();
.withConsistency(REQUEST_PLUS).inCollection(collectionName).count().block();
assertEquals(7, count2);
} finally {
@@ -427,8 +430,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName)
.one(vie.withIcao("lowa")).block();
try {
List<Airport> found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inScope(scopeName).inCollection(collectionName).withOptions(options).all().collectList().block();
List<Airport> found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(scopeName)
.inCollection(collectionName).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 +482,9 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10));
Airport saved = template.insertById(Airport.class).inScope(scopeName).inCollection(collectionName)
.one(vie.withIcao("lowe")).block();
List<RemoveResult> 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<RemoveResult> removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS)
.inScope(scopeName).inCollection(collectionName).withOptions(options)
.matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block();
assertEquals(saved.getId(), removeResults.get(0).getId());
}
@@ -526,13 +528,12 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
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 +572,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection)
.one(vie.withIcao("lowj")).block();
try {
List<Airport> found = template.findByQuery(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inScope(otherScope).inCollection(otherCollection).withOptions(options).all().collectList().block();
List<Airport> found = template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope)
.inCollection(otherCollection).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 +624,9 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
QueryOptions options = QueryOptions.queryOptions().timeout(Duration.ofSeconds(10));
Airport saved = template.insertById(Airport.class).inScope(otherScope).inCollection(otherCollection)
.one(vie.withIcao("lown")).block();
List<RemoveResult> 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<RemoveResult> removeResults = template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS)
.inScope(otherScope).inCollection(otherCollection).withOptions(options)
.matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block();
assertEquals(saved.getId(), removeResults.get(0).getId());
}
@@ -690,7 +690,7 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
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)
() -> template.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope)
.inCollection(otherCollection).withOptions(options).all().collectList().block());
}
@@ -729,8 +729,8 @@ class ReactiveCouchbaseTemplateQueryCollectionIntegrationTests extends Collectio
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)
() -> template.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).inScope(otherScope)
.inCollection(otherCollection).withOptions(options)
.matching(Query.query(QueryCriteria.where("iata").is(vie.getIata()))).all().collectList().block());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors
* 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.
@@ -24,6 +24,7 @@ import org.springframework.data.couchbase.core.mapping.id.GenerationStrategy;
/**
* @author Oliver Gierke
* @author Michael Reiche
*/
@Document
public class AbstractEntity {
@@ -39,6 +40,10 @@ public class AbstractEntity {
return id;
}
public String id() {
return id.toString();
}
/**
* set the id
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2021 the original author or authors.
* Copyright 2017-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.
@@ -17,6 +17,7 @@
package org.springframework.data.couchbase.domain;
import jakarta.validation.constraints.Max;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
@@ -44,8 +45,8 @@ public class Airport extends ComparableEntity {
@CreatedBy private String createdBy;
@Expiration private long expiration;
@Max(2)
long size;
@Max(2) long size;
private long someNumber;
@PersistenceConstructor
public Airport(String key, String iata, String icao) {
@@ -91,11 +92,11 @@ public class Airport extends ComparableEntity {
return createdBy;
}
public long getSize(){
public long getSize() {
return size;
}
public void setSize(long size){
public void setSize(long size) {
this.size = size;
}
}

View File

@@ -64,6 +64,7 @@ public interface AirportRepository extends CouchbaseRepository<Airport, String>,
List<Airport> findByIataInAndIcaoIn(java.util.Collection<String> size, java.util.Collection<String> color,
Pageable pageable);
// override an annotate with REQUEST_PLUS
@Override
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<Airport> findAll();
@@ -90,6 +91,12 @@ public interface AirportRepository extends CouchbaseRepository<Airport, String>,
@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<Airport> groupByIata();
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Airport findArchivedByIata(Iata iata);
// NOT_BOUNDED to test ScanConsistency
// @ScanConsistency(query = QueryScanConsistency.NOT_BOUNDED)
Airport iata(String iata);

View File

@@ -0,0 +1,29 @@
/*
* 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.domain;
/**
* Config to be used for testing scopes and collections.
*
* @author Michael Reiche
*/
public class CollectionsConfig extends Config {
@Override
public String getScopeName() {
return "my_scope";
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors
* Copyright 2021-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.
@@ -13,7 +13,6 @@
* 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;
@@ -30,8 +29,7 @@ import java.util.Random;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -70,8 +68,13 @@ import com.couchbase.client.java.query.QueryScanConsistency;
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
public class FluxIntegrationTests 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.
@@ -85,23 +88,19 @@ public class FluxIntegrationTests extends JavaIntegrationTests {
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<String> keyList = Arrays.asList("a", "b", "c", "d", "e");
static Collection collection;
static ReactiveCollection rCollection;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* 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.
@@ -22,13 +22,20 @@ 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.domain.Persistable;
import org.springframework.lang.Nullable;
/**
* Person entity for tests.
*
* @author Michael Reiche
*/
@Document
public class Person extends AbstractEntity {
public class Person extends AbstractEntity implements Persistable<Object> {
Optional<String> firstname;
@Nullable Optional<String> lastname;
@@ -47,13 +54,18 @@ public class Person extends AbstractEntity {
private Address address;
public Person() {}
@Transient private boolean isNew;
public Person() {
setId(UUID.randomUUID());
}
public Person(String firstname, String lastname) {
this();
setFirstname(firstname);
setLastname(lastname);
setMiddlename("Nick");
isNew(true);
}
public Person(int id, String firstname, String lastname) {
@@ -61,19 +73,24 @@ public class Person extends AbstractEntity {
setId(new UUID(id, id));
}
public Person(UUID id, String firstname, String lastname) {
this(firstname, lastname);
setId(id);
}
static String optional(String name, Optional<String> 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<String> getFirstname() {
return firstname;
public String getFirstname() {
return firstname.get();
}
public void setFirstname(String firstname) {
@@ -84,8 +101,8 @@ public class Person extends AbstractEntity {
this.firstname = firstname;
}
public Optional<String> getLastname() {
return lastname;
public String getLastname() {
return lastname.get();
}
public void setLastname(String lastname) {
@@ -131,7 +148,7 @@ public class Person extends AbstractEntity {
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 +165,48 @@ public class Person extends AbstractEntity {
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;
return p;
}
// A with-er that returns the same object ??
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;
}
public Person withIdFirstname() {
return this.withFirstName(getId().toString());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* 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.
@@ -18,9 +18,10 @@ package org.springframework.data.couchbase.domain;
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 @@ import com.couchbase.client.java.query.QueryScanConsistency;
/**
* @author Michael Reiche
*/
public interface PersonRepository extends CrudRepository<Person, String> {
public interface PersonRepository extends CouchbaseRepository<Person, String>, DynamicProxyable<PersonRepository> {
/*
* These methods are exercised in HomeController of the test spring-boot DemoApplication
@@ -95,7 +96,7 @@ public interface PersonRepository extends CrudRepository<Person, String> {
boolean existsById(UUID var1);
Iterable<Person> findAll();
List<Person> findAll();
long count();

View File

@@ -0,0 +1,49 @@
/*
* 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.domain;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.couchbase.core.mapping.Document;
/**
* Person entity without a an @Version property
*
* @author Michael Reiche
*/
@Document
public class PersonWithoutVersion extends AbstractEntity {
Optional<String> firstname;
Optional<String> lastname;
public PersonWithoutVersion() {
firstname = Optional.empty();
lastname = Optional.empty();
}
public PersonWithoutVersion(String firstname, String lastname) {
this.firstname = Optional.of(firstname);
this.lastname = Optional.of(lastname);
setId(UUID.randomUUID());
}
public PersonWithoutVersion(UUID id, String firstname, String lastname) {
this.firstname = Optional.of(firstname);
this.lastname = Optional.of(lastname);
setId(id);
}
}

View File

@@ -46,6 +46,10 @@ import com.couchbase.client.java.query.QueryScanConsistency;
public interface ReactiveAirportRepository
extends ReactiveCouchbaseRepository<Airport, String>, DynamicProxyable<ReactiveAirportRepository> {
@Query("SELECT META(#{#n1ql.bucket}).id AS __id, META(#{#n1ql.bucket}).cas AS __cas, meta().id as id FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} #{[1]}")
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Flux<String> findIdByDynamicN1ql(String docType, String queryStatement);
@Override
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Flux<Airport> findAll();

View File

@@ -0,0 +1,27 @@
/*
* 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.domain;
import org.springframework.data.couchbase.repository.DynamicProxyable;
import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository;
/**
* @author Michael Reiche
*/
public interface ReactivePersonRepository
extends ReactiveCouchbaseRepository<Person, String>, DynamicProxyable<ReactivePersonRepository> {
}

View File

@@ -76,4 +76,7 @@ public interface UserRepository extends CouchbaseRepository<User, String> {
} catch (InterruptedException ie) {}
return findByFirstname(firstname);
}
@Override
User save(User user);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.couchbase.repository;
import static com.couchbase.client.java.query.QueryOptions.queryOptions;
import static com.couchbase.client.java.query.QueryScanConsistency.NOT_BOUNDED;
import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static java.util.Arrays.asList;
@@ -108,6 +109,7 @@ import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.kv.GetResult;
import com.couchbase.client.java.kv.InsertOptions;
import com.couchbase.client.java.kv.MutationState;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency;
@@ -141,6 +143,7 @@ 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.removeByQuery(Airport.class).withConsistency(REQUEST_PLUS).all();
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).all();
@@ -283,8 +286,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
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);
@@ -356,6 +358,9 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
@Test
public void saveNotBoundedWithDefaultRepository() {
if (config().isUsingCloud()) { // I don't think the query following the insert will be quick enough for the test
return;
}
airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll();
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
// the Config class has been modified, these need to be loaded again
@@ -363,16 +368,21 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
AirportRepositoryScanConsistencyTest airportRepositoryRP = (AirportRepositoryScanConsistencyTest) ac
.getBean("airportRepositoryScanConsistencyTest");
List<Airport> sizeBeforeTest = airportRepositoryRP.findAll();
List<Airport> sizeBeforeTest = (List<Airport>)airportRepositoryRP.findAll();
assertEquals(0, sizeBeforeTest.size());
Airport vie = new Airport("airports::vie", "vie", "low9");
Airport saved = airportRepositoryRP.save(vie);
List<Airport> allSaved = airportRepositoryRP.findAll();
couchbaseTemplate.removeById(Airport.class).one(saved.getId());
if (!config().isUsingCloud()) {
assertTrue(allSaved.isEmpty(), "should not have been empty");
boolean notFound = false;
for (int i = 0; i < 100; i++) {
Airport vie = new Airport("airports::vie", "vie", "low9");
Airport saved = airportRepositoryRP.save(vie);
List<Airport> allSaved = (List<Airport>)airportRepositoryRP.findAll();
couchbaseTemplate.removeById(Airport.class).one(saved.getId());
if (allSaved.isEmpty()) {
notFound = true;
break;
}
}
assertTrue(notFound, "the doc should not have been found. maybe");
}
@Test
@@ -388,7 +398,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
Airport vie = new Airport("airports::vie", "vie", "low9");
Airport saved = airportRepositoryRP.save(vie);
List<Airport> allSaved = airportRepositoryRP.findAll();
List<Airport> allSaved = airportRepositoryRP.findAll(REQUEST_PLUS);
couchbaseTemplate.removeById(Airport.class).one(saved.getId());
assertEquals(1, allSaved.size(), "should have found 1 airport");
}
@@ -399,8 +409,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
try {
vie = new Airport("airports::vie", "vie", "loww");
vie = airportRepository.save(vie);
List<Airport> airports = couchbaseTemplate.findByQuery(Airport.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS)
List<Airport> airports = couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS)
.matching(org.springframework.data.couchbase.core.query.Query
.query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport")))
.all();
@@ -462,15 +471,13 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
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
@@ -478,7 +485,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
() -> 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);
@@ -512,12 +520,11 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
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 {
@@ -535,7 +542,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
// 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(NOT_BOUNDED))
.iata(saved.getIata());
if (airport2 == null) {
break;
}
@@ -548,7 +556,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
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");
}
}
@@ -717,6 +726,24 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
}
}
@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<Airport> 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());
@@ -872,7 +899,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
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));

View File

@@ -59,10 +59,8 @@ import com.couchbase.client.java.env.ClusterEnvironment;
public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareIntegrationTests {
@Autowired ReactiveUserRepository userRepository;
@Autowired ReactiveAirportRepository reactiveAirportRepository;
@Autowired ReactiveAirlineRepository airlineRepository;
@Autowired ReactiveAirlineRepository reactiveAirlineRepository;
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
@@ -82,9 +80,10 @@ public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends Cluster
// Airline does not have a version
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null);
// save the document - we don't care how on this call
airlineRepository.save(airline).block();
airlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace.
airlineRepository.delete(airline).block();
reactiveAirlineRepository.save(airline).block();
reactiveAirlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or
// replace.
reactiveAirlineRepository.delete(airline).block();
}
@Test

View File

@@ -96,6 +96,28 @@ public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegr
}
}
@Test
void testQuery() {
Airport vie = null;
Airport jfk = null;
try {
vie = new Airport("airports::vie", "vie", "low1");
reactiveAirportRepository.save(vie).block();
jfk = new Airport("airports::jfk", "JFK", "xxxx");
reactiveAirportRepository.save(jfk).block();
List<String> all = reactiveAirportRepository.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 {
reactiveAirportRepository.delete(vie).block();
reactiveAirportRepository.delete(jfk).block();
}
}
@Test
void findBySimpleProperty() {
Airport vie = null;
@@ -219,7 +241,7 @@ public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegr
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 {
@@ -242,7 +264,7 @@ public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegr
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 {

View File

@@ -31,12 +31,15 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.AirportRepositoryAnnotated;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.domain.CollectionsConfig;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserCol;
import org.springframework.data.couchbase.domain.UserColRepository;
@@ -60,15 +63,18 @@ import com.couchbase.client.java.query.QueryOptions;
*
* @author Michael Reiche
*/
@SpringJUnitConfig(Config.class)
@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 AirportRepositoryAnnotated airportRepositoryAnnotated;
@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() {
@@ -222,9 +228,14 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
// 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<RemoveResult> removed = couchbaseTemplate.removeByQuery(Airport.class).all();
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).all();
}
}
// template default scope is my_scope

View File

@@ -27,8 +27,10 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.Config;
import org.springframework.data.couchbase.domain.CollectionsConfig;
import org.springframework.data.couchbase.domain.ReactiveAirportRepository;
import org.springframework.data.couchbase.domain.ReactiveAirportRepositoryAnnotated;
import org.springframework.data.couchbase.domain.ReactiveUserColRepository;
@@ -38,26 +40,28 @@ import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.error.IndexFailureException;
import com.couchbase.client.core.io.CollectionIdentifier;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* Reactive Repository Query Tests with Collections
*
* @author Michael Reiche
*/
@SpringJUnitConfig(Config.class)
@SpringJUnitConfig(CollectionsConfig.class)
@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED)
public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests {
@Autowired ReactiveAirportRepository reactiveAirportRepository;
@Autowired ReactiveAirportRepositoryAnnotated reactiveAirportRepositoryAnnotated;
@Autowired ReactiveUserColRepository userColRepository;
@Autowired public CouchbaseTemplate couchbaseTemplate;
@Autowired public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;
@BeforeAll
public static void beforeAll() {
@@ -221,9 +225,10 @@ public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends
Airport otherAirport = new Airport(loc(), "xxx", "xyz");
try {
airport = reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).save(airport).block();
otherAirport = reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).save(otherAirport).block();
assertEquals(1,
reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).deleteByIata(airport.getIata()).collectList().block().size());
otherAirport = reactiveAirportRepository.withScope(scopeName).withCollection(collectionName).save(otherAirport)
.block();
assertEquals(1, reactiveAirportRepository.withScope(scopeName).withCollection(collectionName)
.deleteByIata(airport.getIata()).collectList().block().size());
} catch (Exception e) {
e.printStackTrace();
throw e;
@@ -240,7 +245,8 @@ public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends
airport = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(airport).block();
otherAirport = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(otherAirport).block();
// don't specify a collection - should get collection from AirportRepositoryAnnotated
assertEquals(1, reactiveAirportRepositoryAnnotated.withScope(scopeName).deleteByIata(airport.getIata()).collectList().block().size());
assertEquals(1, reactiveAirportRepositoryAnnotated.withScope(scopeName).deleteByIata(airport.getIata())
.collectList().block().size());
} catch (Exception e) {
e.printStackTrace();
throw e;
@@ -258,8 +264,8 @@ public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends
Airport airportSaved = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(airport).block();
Airport otherAirportSaved = reactiveAirportRepositoryAnnotated.withScope(scopeName).save(otherAirport).block();
// don't specify a collection - should get collection from deleteByIataAnnotated method
assertThrows(IndexFailureException.class, () -> assertEquals(1,
reactiveAirportRepositoryAnnotated.withScope(scopeName).deleteByIataAnnotated(airport.getIata()).collectList().block().size()));
assertThrows(IndexFailureException.class, () -> assertEquals(1, reactiveAirportRepositoryAnnotated
.withScope(scopeName).deleteByIataAnnotated(airport.getIata()).collectList().block().size()));
} catch (Exception e) {
e.printStackTrace();
throw e;

View File

@@ -0,0 +1,49 @@
/*
* 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 lombok.Data;
import org.springframework.data.domain.Persistable;
/**
* For testing transactions.
*
* @author Michael Reiche
*/
@Data
public class AfterTransactionAssertion<T extends Persistable> {
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;
}
}

View File

@@ -0,0 +1,348 @@
/*
* Copyright 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import lombok.Data;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
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.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.couchbase.CouchbaseClientFactory;
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.core.TransactionalSupport;
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.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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.DocumentExistsException;
import com.couchbase.client.java.transactions.TransactionResult;
/**
* Tests for com.couchbase.transactions using
* <li><le>couchbase reactive transaction manager via transactional operator</le> <le>couchbase non-reactive transaction
* manager via @Transactional</le> <le>@Transactional(transactionManager =
* BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER)</le></li>
*
* @author Michael Reiche
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(classes = { TransactionsConfig.class, PersonService.class })
public class CouchbasePersonTransactionIntegrationTests extends JavaIntegrationTests {
// intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated.
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonRepository repo;
@Autowired ReactivePersonRepository rxRepo;
@Autowired CouchbaseTemplate cbTmpl;
@Autowired ReactiveCouchbaseTemplate rxCBTmpl;
@Autowired PersonService personService;
@Autowired TransactionalOperator transactionalOperator;
String sName = "_default";
String cName = "_default";
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@AfterEach
public void afterEachTest() {
TransactionTestUtil.assertNotInTransaction();
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
List<RemoveResult> rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<RemoveResult> rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName)
.inCollection(cName).all();
List<RemoveResult> rp2 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all();
List<RemoveResult> rp3 = cbTmpl.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName)
.inCollection(cName).all();
List<Person> p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<Person> p1 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName)
.all();
List<EventLog> e0 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all();
List<EventLog> e1 = cbTmpl.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).inScope(sName)
.inCollection(cName).all();
}
@DisplayName("rollback after exception using transactionalOperator")
@Test
public void shouldRollbackAfterException() {
assertThrowsWithCause(() -> personService.savePersonErrors(WalterWhite),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(0, count, "should have done roll back and left 0 entries");
}
@Test
@DisplayName("rollback after exception using @Transactional")
public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() {
assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(0, count, "should have done roll back and left 0 entries");
}
@Test
@DisplayName("rollback after exception after using @Transactional(reactive)")
public void shouldRollbackAfterExceptionOfTxAnnotatedMethodReactive() {
assertThrowsWithCause(() -> personService.declarativeSavePersonErrorsReactive(WalterWhite).block(),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Long count = cbTmpl.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 = personService.savePerson(WalterWhite);
Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(1, count, "should have saved and found 1");
}
@Test
public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() {
Person p = personService.declarativeSavePerson(WalterWhite);
Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(1, count, "should have saved and found 1");
}
@Test
/**
* This fails with TransactionOperationFailedException {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 replaceInTxAnnotatedCallback() {
Person person = cbTmpl.insertById(Person.class).one(WalterWhite);
Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds");
AtomicInteger tryCount = new AtomicInteger(0);
Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount);
Person pFound = cbTmpl.findById(Person.class).one(person.id());
assertEquals(switchedPerson.getFirstname(), pFound.getFirstname(), "should have been switched");
}
@Test
public void commitShouldPersistTxEntriesOfTxAnnotatedMethodReactive() {
Person p = personService.declarativeSavePersonReactive(WalterWhite).block();
Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(1, count, "should have saved and found 1");
}
@Test
public void commitShouldPersistTxEntriesAcrossCollections() {
List<EventLog> persons = personService.saveWithLogs(WalterWhite);
Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(1, count, "should have saved and found 1");
Long countEvents = cbTmpl.count(new Query(), EventLog.class); //
assertEquals(4, countEvents, "should have saved and found 4");
}
@Test
public void rollbackShouldAbortAcrossCollections() {
assertThrowsWithCause(() -> personService.saveWithErrorLogs(WalterWhite),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
List<Person> persons = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
assertEquals(0, persons.size(), "should have done roll back and left 0 entries");
List<EventLog> events = cbTmpl.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(WalterWhite);
assertEquals(1, count, "should have counted 1 during tx");
}
@Test
public void emitMultipleElementsDuringTransaction() {
List<EventLog> docs = personService.saveWithLogs(WalterWhite);
assertEquals(4, docs.size(), "should have found 4 eventlogs");
}
@Test
public void errorAfterTxShouldNotAffectPreviousStep() {
Person p = personService.savePerson(WalterWhite);
assertThrowsOneOf(() -> personService.savePerson(p), TransactionSystemUnambiguousException.class,
DocumentExistsException.class);
Long count = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(1, count, "should have saved and found 1");
}
@Test
public void replacePersonCBTransactionsRxTmpl() {
Person person = cbTmpl.insertById(Person.class).one(WalterWhite);
Mono<Person> result = rxCBTmpl.findById(Person.class).one(person.id()) //
.flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp)).doOnNext(ppp -> TransactionalSupport
.checkForTransactionInThreadLocalStorage().doOnNext(v -> assertTrue(v.isPresent())))
.as(transactionalOperator::transactional);
result.block();
Person pFound = cbTmpl.findById(Person.class).one(person.id());
assertEquals(person, pFound, "should have found expected " + person);
}
@Test
public void insertPersonCBTransactionsRxTmplRollback() {
Mono<Person> result = rxCBTmpl.insertById(Person.class).one(WalterWhite) //
.doOnNext(ppp -> TransactionalSupport.checkForTransactionInThreadLocalStorage()
.doOnNext(v -> assertTrue(v.isPresent())))
.map(p -> throwSimulateFailureException(p)).as(transactionalOperator::transactional); // tx
assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).one(WalterWhite.id());
assertNull(pFound, "insert should have been rolled back");
}
@Test
public void insertTwicePersonCBTransactionsRxTmplRollback() {
Mono<Person> result = rxCBTmpl.insertById(Person.class).one(WalterWhite) //
.flatMap(ppp -> rxCBTmpl.insertById(Person.class).one(ppp)) //
.as(transactionalOperator::transactional);
assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, DuplicateKeyException.class);
Person pFound = cbTmpl.findById(Person.class).one(WalterWhite.id());
assertNull(pFound, "insert should have been rolled back");
}
/**
* I think this test might fail sometimes? Does it need retryWhen() ?
*/
@Disabled("todo gp: disabling temporarily as hanging intermittently")
@Test
public void wrapperReplaceWithCasConflictResolvedViaRetry() {
AtomicInteger tryCount = new AtomicInteger();
Person person = cbTmpl.insertById(Person.class).one(WalterWhite);
String newName = "Dave";
TransactionResult txResult = couchbaseClientFactory.getCluster().transactions().run(ctx -> {
Person ppp = cbTmpl.findById(Person.class).one(person.id());
ReplaceLoopThread.updateOutOfTransaction(cbTmpl, person, tryCount.incrementAndGet());
Person pppp = cbTmpl.replaceById(Person.class).one(ppp.withFirstName(newName));
});
Person pFound = cbTmpl.findById(Person.class).one(person.id());
assertTrue(tryCount.get() > 1, "should have been more than one try. tries: " + tryCount.get());
assertEquals(newName, pFound.getFirstname(), "should have been switched");
}
/**
* This does process retries - by CallbackTransactionManager.execute() -> transactions.run() -> executeTransaction()
* -> retryWhen.
*/
/**
* This fails with TransactionOperationFailedException {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
*/
@Test
public void replaceWithCasConflictResolvedViaRetryAnnotatedCallback() {
Person person = cbTmpl.insertById(Person.class).one(WalterWhite);
Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds");
AtomicInteger tryCount = new AtomicInteger();
Person p = personService.declarativeFindReplacePersonCallback(switchedPerson, tryCount);
Person pFound = cbTmpl.findById(Person.class).one(person.id());
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 = cbTmpl.insertById(Person.class).one(WalterWhite);
Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds");
AtomicInteger tryCount = new AtomicInteger();
Person res = personService.declarativeFindReplacePersonReactive(switchedPerson, tryCount).block();
Person pFound = cbTmpl.findById(Person.class).one(person.id());
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 replaceWithCasConflictResolvedViaRetryAnnotated() {
Person person = cbTmpl.insertById(Person.class).one(WalterWhite);
Person switchedPerson = person.withFirstName("Dave");
AtomicInteger tryCount = new AtomicInteger();
Person p = personService.declarativeFindReplacePerson(switchedPerson, tryCount);
Person pFound = cbTmpl.findById(Person.class).one(person.id());
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());
}
@Data
static class EventLog {
public EventLog() {}; // don't remove this
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();
}
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import lombok.Data;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
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.data.annotation.Version;
import org.springframework.data.couchbase.CouchbaseClientFactory;
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.Person;
import org.springframework.data.couchbase.domain.PersonRepository;
import org.springframework.data.couchbase.domain.ReactivePersonRepository;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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.java.Cluster;
/**
* todo gp: these tests are using the `.as(transactionalOperator::transactional)` method which is for the chopping
* block, so presumably these tests are too
* todo mr: I'm not sure how as(transactionalOperator::transactional) is different than
* todo mr: transactionOperator.transaction(...)in CouchbaseTransactionalOperatorTemplateIntegrationTests ?
*
* @author Michael Reiche
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(classes = { TransactionsConfig.class, PersonServiceReactive.class })
public class CouchbasePersonTransactionReactiveIntegrationTests extends JavaIntegrationTests {
// intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated.
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired ReactivePersonRepository rxRepo;
@Autowired PersonRepository repo;
@Autowired CouchbaseTemplate cbTmpl;
@Autowired ReactiveCouchbaseTemplate rxCBTmpl;
@Autowired Cluster myCluster;
@Autowired PersonServiceReactive personService;
@Autowired ReactiveCouchbaseTemplate operations;
// if these are changed from default, then beforeEach needs to clean up separately
String sName = "_default";
String cName = "_default";
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
List<RemoveResult> pr = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList()
.block();
List<RemoveResult> er = operations.removeByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList()
.block();
List<Person> p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().collectList().block();
List<EventLog> e = operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).all().collectList().block();
}
@Test
public void shouldRollbackAfterException() {
personService.savePersonErrors(WalterWhite) //
.as(StepVerifier::create) //
.verifyError(TransactionSystemUnambiguousException.class);
operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() //
.as(StepVerifier::create) //
.expectNext(0L) //
.verifyComplete();
}
@Test
public void shouldRollbackAfterExceptionOfTxAnnotatedMethod() {
assertThrowsWithCause(() -> personService.declarativeSavePersonErrors(WalterWhite).blockLast(),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
}
@Test
public void commitShouldPersistTxEntries() {
personService.savePerson(WalterWhite) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() //
.as(StepVerifier::create) //
.expectNext(1L) //
.verifyComplete();
}
@Test
public void commitShouldPersistTxEntriesOfTxAnnotatedMethod() {
personService.declarativeSavePerson(WalterWhite).as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() //
.as(StepVerifier::create) //
.expectNext(1L) //
.verifyComplete();
}
@Test
public void commitShouldPersistTxEntriesAcrossCollections() {
personService.saveWithLogs(WalterWhite) //
.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
public void rollbackShouldAbortAcrossCollections() {
personService.saveWithErrorLogs(WalterWhite) //
.then() //
.as(StepVerifier::create) //
.verifyError();
operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() //
.as(StepVerifier::create) //
.expectNext(0L) //
.verifyComplete();
operations.findByQuery(EventLog.class).withConsistency(REQUEST_PLUS).count()//
.as(StepVerifier::create) //
.expectNext(0L) //
.verifyComplete();
}
@Test
public void countShouldWorkInsideTransaction() {
personService.countDuringTx(WalterWhite) //
.as(StepVerifier::create) //
.expectNext(1L) //
.verifyComplete();
}
@Test
public void emitMultipleElementsDuringTransaction() {
personService.saveWithLogs(WalterWhite) //
.as(StepVerifier::create) //
.expectNextCount(4L) //
.verifyComplete();
}
@Test
public void errorAfterTxShouldNotAffectPreviousStep() {
personService.savePerson(WalterWhite) //
.then(Mono.error(new SimulateFailureException())).as(StepVerifier::create) //
.verifyError();
operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count() //
.as(StepVerifier::create) //
.expectNext(1L) //
.verifyComplete();
}
@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;
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
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.data.couchbase.CouchbaseClientFactory;
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.Person;
import org.springframework.data.couchbase.domain.PersonRepository;
import org.springframework.data.couchbase.domain.ReactivePersonRepository;
import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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;
/**
* Tests for CouchbaseTransactionalOperator.
*
* @author Michael Reiche
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
public class CouchbaseReactiveTransactionNativeIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired ReactivePersonRepository rxRepo;
@Autowired PersonRepository repo;
@Autowired CouchbaseTemplate cbTmpl;
@Autowired ReactiveCouchbaseTemplate rxCBTmpl;
@Autowired ReactiveCouchbaseTemplate operations;
// This will pick up CouchbaseTransactionalOperator
@Autowired TransactionalOperator txOperator;
String sName = "_default";
String cName = "_default";
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
assertTrue(txOperator instanceof CouchbaseTransactionalOperator);
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
TransactionTestUtil.assertNotInTransaction();
List<RemoveResult> rp0 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<RemoveResult> rp1 = cbTmpl.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName)
.inCollection(cName).all();
List<Person> p0 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<Person> p1 = cbTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName)
.all();
}
@Test
public void replacePersonTemplate() {
Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block();
Flux<Person> result = txOperator.execute((ctx) -> rxCBTmpl.findById(Person.class).one(person.id())
.flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))));
result.blockLast();
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt");
}
@Test
public void replacePersonRbTemplate() {
Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block();
Flux<Person> result = txOperator.execute((ctx) -> rxCBTmpl.findById(Person.class).one(person.id())
.flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))
.map(it -> throwSimulateFailureException(it)));
assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class,
SimulateFailureException.class);
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals(person, pFound, "Should have found " + person);
}
@Test
public void insertPersonTemplate() {
Person person = WalterWhite;
Flux<Person> result = txOperator.execute((ctx) -> rxCBTmpl.insertById(Person.class).one(person)
.flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))));
result.blockLast();
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt");
}
@Test
public void insertPersonRbTemplate() {
Person person = WalterWhite;
Flux<Person> result = txOperator.execute((ctx) -> rxCBTmpl.insertById(Person.class).one(person)
.flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))
.map(it -> throwSimulateFailureException(it)));
assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class,
SimulateFailureException.class);
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertNull(pFound, "Should NOT have found " + pFound);
}
@Test
public void replacePersonRbRepo() {
Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block();
Flux<Person> result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).findById(person.id())
.flatMap(p -> rxRepo.withCollection(cName).save(p.withFirstName("Walt")))
.flatMap(it -> Mono.error(new SimulateFailureException())));
assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class,
SimulateFailureException.class);
Person pFound = rxRepo.withCollection(cName).findById(person.id()).block();
assertEquals(person, pFound, "Should have found " + person);
}
@Test
public void insertPersonRbRepo() {
Person person = WalterWhite;
Flux<Person> result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).save(person) // insert
.map(it -> throwSimulateFailureException(it)));
assertThrowsWithCause(result::blockLast, TransactionSystemUnambiguousException.class,
SimulateFailureException.class);
Person pFound = rxRepo.withCollection(cName).findById(person.id()).block();
assertNull(pFound, "Should NOT have found " + pFound);
}
@Test
public void insertPersonRepo() {
Person person = WalterWhite;
Flux<Person> result = txOperator.execute((ctx) -> rxRepo.withCollection(cName).save(person) // insert
.flatMap(p -> rxRepo.withCollection(cName).save(p.withFirstName("Walt"))));
result.blockLast();
Person pFound = rxRepo.withCollection(cName).findById(person.id()).block();
assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt");
}
@Test
public void replacePersonSpringTransactional() {
Person person = WalterWhite;
rxCBTmpl.insertById(Person.class).inCollection(cName).one(person).block();
Mono<?> result = rxCBTmpl.findById(Person.class).one(person.id())
.flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt"))).as(txOperator::transactional);
result.block();
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals(person.withFirstName("Walt"), pFound, "Should have found " + person);
}
@Test
public void replacePersonRbSpringTransactional() {
Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block();
Mono<?> result = rxCBTmpl.findById(Person.class).one(person.id())
.flatMap(p -> rxCBTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))
.flatMap(it -> Mono.error(new SimulateFailureException())).as(txOperator::transactional);
assertThrowsWithCause(result::block, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals(person, pFound, "Should have found " + person);
assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be " + person.getFirstname());
}
@Test
public void findReplacePersonCBTransactionsRxTmpl() {
Person person = rxCBTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite).block();
Flux<Person> result = txOperator.execute(ctx -> rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id())
.flatMap(pGet -> rxCBTmpl.replaceById(Person.class).inCollection(cName).one(pGet.withFirstName("Walt"))));
result.blockLast();
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt");
}
@Test
public void insertReplacePersonsCBTransactionsRxTmpl() {
Person person = WalterWhite;
Flux<Person> result = txOperator.execute((ctx) -> rxCBTmpl.insertById(Person.class).inCollection(cName).one(person)
.flatMap(pInsert -> rxCBTmpl.replaceById(Person.class).inCollection(cName).one(pInsert.withFirstName("Walt"))));
result.blockLast();
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals(person.withFirstName("Walt"), pFound, "Should have found Walt");
}
@Test
void transactionalSavePerson() {
Person person = WalterWhite;
savePerson(person).block();
Person pFound = rxCBTmpl.findById(Person.class).inCollection(cName).one(person.id()).block();
assertEquals(person, pFound, "Should have found " + person);
}
public Mono<Person> savePerson(Person person) {
return operations.save(person) //
.as(txOperator::transactional);
}
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
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.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
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.transaction.CouchbaseTransactionalOperator;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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.TransactionManager;
import org.springframework.transaction.reactive.TransactionalOperator;
/**
* Tests for com.couchbase.transactions without using the spring data transactions framework
* <p>
* Tests CouchbaseTransactionalOperator.
*
* @author Michael Reiche
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
// I think these are all redundant (see CouchbaseReactiveTransactionNativeTests). There does not seem to be a blocking
// form of TransactionalOperator. Also there does not seem to be a need for a CouchbaseTransactionalOperator as
// TransactionalOperator.create(reactiveCouchbaseTransactionManager) seems to work just fine. (I don't recall what
// merits the "Native" in the name).
public class CouchbaseTransactionNativeIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired TransactionManager couchbaseTransactionManager;
@Autowired PersonRepository repo;
@Autowired ReactivePersonRepository repoRx;
@Autowired CouchbaseTemplate cbTmpl;
@Autowired ReactiveCouchbaseTemplate rxCbTmpl;
@Autowired TransactionalOperator txOperator;
static String cName; // short name
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
// short names
cName = null;// cName;
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEach() {
assertTrue(txOperator instanceof CouchbaseTransactionalOperator);
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
}
@AfterEach
public void afterEach() {
TransactionTestUtil.assertNotInTransaction();
}
@Test
public void replacePersonTemplate() {
Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite);
assertThrowsWithCause(() -> txOperator.execute((ctx) -> rxCbTmpl.findById(Person.class).one(person.id()) //
.flatMap(pp -> rxCbTmpl.replaceById(Person.class).one(pp.withIdFirstname()) //
.map(ppp -> throwSimulateFailureException(ppp))))
.blockLast(), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString());
assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be " + person.getFirstname());
}
@Test
public void replacePersonRbTemplate() {
Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite);
assertThrowsWithCause(
() -> txOperator.execute((ctx) -> rxCbTmpl.findById(Person.class).one(person.getId().toString()) //
.flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withIdFirstname())) //
.map(ppp -> throwSimulateFailureException(ppp))).blockLast(), //
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.getId().toString());
assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be " + person.getFirstname());
}
@Test
public void insertPersonTemplate() {
txOperator.execute((ctx) -> rxCbTmpl.insertById(Person.class).one(WalterWhite)
.flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))).blockLast();
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(WalterWhite.id());
assertEquals("Walt", pFound.getFirstname(), "firstname should be Walt");
}
@Test
public void insertPersonRbTemplate() {
assertThrowsWithCause(
() -> txOperator.execute((ctx) -> rxCbTmpl.insertById(Person.class).one(WalterWhite)
.flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))
.map(it -> throwSimulateFailureException(it))).blockLast(),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(WalterWhite.id());
assertNull(pFound, "Should NOT have found " + pFound);
}
@Test
public void replacePersonRbRepo() {
Person person = repo.withCollection(cName).save(WalterWhite);
assertThrowsWithCause(() -> txOperator.execute(ctx -> {
return repoRx.withCollection(cName).findById(person.id())
.flatMap(p -> repoRx.withCollection(cName).save(p.withFirstName("Walt")))
.map(pp -> throwSimulateFailureException(pp));
}).blockLast(), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id());
assertEquals(person, pFound, "Should have found " + person);
}
@Test
public void insertPersonRbRepo() {
assertThrowsWithCause(() -> txOperator.execute((ctx) -> repoRx.withCollection(cName).save(WalterWhite) // insert
.flatMap(p -> repoRx.withCollection(cName).save(p.withFirstName("Walt"))) // replace
.map(it -> throwSimulateFailureException(it))).blockLast(), TransactionSystemUnambiguousException.class,
SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(WalterWhite.id());
assertNull(pFound, "Should NOT have found " + pFound);
}
@Test
public void insertPersonRepo() {
txOperator.execute((ctx) -> repoRx.withCollection(cName).save(WalterWhite) // insert
.flatMap(p -> repoRx.withCollection(cName).save(p.withFirstName("Walt"))) // replace
).blockFirst();
Optional<Person> pFound = repo.withCollection(cName).findById(WalterWhite.id());
assertEquals("Walt", pFound.get().getFirstname(), "firstname should be Walt");
}
@Test
public void replacePersonRbSpringTransactional() {
Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite);
assertThrowsWithCause(
() -> txOperator.execute((ctx) -> rxCbTmpl.findById(Person.class).one(person.getId().toString())
.flatMap(p -> rxCbTmpl.replaceById(Person.class).one(p.withFirstName("Walt")))
.map(it -> throwSimulateFailureException(it))).blockLast(),
TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id());
assertEquals(person.getFirstname(), pFound.getFirstname(), "firstname should be Walter");
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 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 static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
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.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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;
/**
* Tests for @Transactional methods, where operations that aren't supported in a transaction are being used. They should
* be prevented at runtime.
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(classes = { TransactionsConfig.class,
CouchbaseTransactionalNonAllowableOperationsIntegrationTests.PersonService.class })
public class CouchbaseTransactionalNonAllowableOperationsIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
}
void test(Consumer<CouchbaseOperations> r) {
AtomicInteger tryCount = new AtomicInteger(0);
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, (ops) -> {
r.accept(ops);
return null;
});
}, TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
assertEquals(1, tryCount.get());
}
@DisplayName("Using existsById() in a transaction is rejected at runtime")
@Test
public void existsById() {
test((ops) -> {
ops.existsById(Person.class).one(WalterWhite.id());
});
}
@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) -> {
ops.findFromReplicasById(Person.class).any(WalterWhite.id());
});
}
@DisplayName("Using upsertById() in a transaction is rejected at runtime")
@Test
public void upsertById() {
test((ops) -> {
ops.upsertById(Person.class).one(WalterWhite);
});
}
@Service
@Component
@EnableTransactionManagement
static class PersonService {
final CouchbaseOperations personOperations;
public PersonService(CouchbaseOperations ops) {
personOperations = ops;
}
@Transactional
public <T> T doInTransaction(AtomicInteger tryCount, Function<CouchbaseOperations, T> callback) {
tryCount.incrementAndGet();
return callback.apply(personOperations);
}
}
}

View File

@@ -0,0 +1,329 @@
/*
* Copyright 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 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;
import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
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.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager;
import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
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;
/**
* Tests for CouchbaseTransactionalOperator, using template methods (findById etc.)
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
public class CouchbaseTransactionalOperatorTemplateIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired ReactiveCouchbaseTemplate ops;
@Autowired CouchbaseTemplate blocking;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
assertNotInTransaction();
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
static class RunResult {
public final int attempts;
public RunResult(int attempts) {
this.attempts = attempts;
}
}
private RunResult doMonoInTransaction(Supplier<Mono<?>> lambda) {
CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager(couchbaseClientFactory);
TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager);
AtomicInteger attempts = new AtomicInteger();
operator.transactional(Mono.fromRunnable(() -> attempts.incrementAndGet()).then(lambda.get())).block();
assertNotInTransaction();
return new RunResult(attempts.get());
}
@DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.execute should succeed")
@Test
public void committedInsertWithExecute() {
CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager(couchbaseClientFactory);
TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager);
operator.execute(v -> {
return Mono.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite);
});
}).blockLast();
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertEquals(WalterWhite.getFirstname(), fetched.getFirstname());
}
@DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.transactional(Flux) should succeed")
@Test
public void committedInsertWithFlux() {
CouchbaseCallbackTransactionManager manager = new CouchbaseCallbackTransactionManager(couchbaseClientFactory);
TransactionalOperator operator = CouchbaseTransactionalOperator.create(manager);
Flux<Person> flux = Flux.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite);
});
operator.transactional(flux).blockLast();
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertEquals(WalterWhite.getFirstname(), fetched.getFirstname());
}
@DisplayName("A basic golden path insert using CouchbaseSimpleTransactionalOperator.transactional(Mono) should succeed")
@Test
public void committedInsert() {
RunResult rr = doMonoInTransaction(() -> {
return Mono.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite);
});
});
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertEquals(WalterWhite.getFirstname(), fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path replace should succeed")
@Test
public void committedReplace() {
Person p = blocking.insertById(Person.class).one(WalterWhite);
RunResult rr = doMonoInTransaction(() -> {
return ops.findById(Person.class).one(WalterWhite.id()).flatMap(person -> {
person.setFirstname("changed");
return ops.replaceById(Person.class).one(person);
});
});
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertEquals("changed", fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path remove should succeed")
@Test
public void committedRemove() {
Person person = blocking.insertById(Person.class).one(WalterWhite);
RunResult rr = doMonoInTransaction(() -> {
return ops.findById(Person.class).one(person.id())
.flatMap(fetched -> ops.removeById(Person.class).oneEntity(fetched));
});
Person fetched = blocking.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path removeByQuery should succeed")
@Test
public void committedRemoveByQuery() {
Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doMonoInTransaction(() -> {
return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS)
.matching(QueryCriteria.where("firstname").eq(person.id())).all().next();
});
Person fetched = blocking.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path findByQuery should succeed")
@Test
public void committedFindByQuery() {
Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doMonoInTransaction(() -> {
return ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS)
.matching(QueryCriteria.where("firstname").eq(person.id())).all().next();
});
assertEquals(1, rr.attempts);
}
@DisplayName("Basic test of doing an insert then rolling back")
@Test
public void rollbackInsert() {
AtomicInteger attempts = new AtomicInteger();
assertThrowsWithCause(() -> doMonoInTransaction(() -> {
attempts.incrementAndGet();
return ops.insertById(Person.class).one(WalterWhite).map((p) -> throwSimulateFailureException(p));
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(WalterWhite.toString());
assertNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a replace then rolling back")
@Test
public void rollbackReplace() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doMonoInTransaction(() -> {
attempts.incrementAndGet();
return ops.findById(Person.class).one(person.id()) //
.flatMap(p -> ops.replaceById(Person.class).one(p.withFirstName("changed"))) //
.map(p -> throwSimulateFailureException(p));
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(person.id());
assertEquals(person.getFirstname(), fetched.getFirstname());
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a remove then rolling back")
@Test
public void rollbackRemove() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doMonoInTransaction(() -> {
attempts.incrementAndGet();
return ops.findById(Person.class).one(person.id()).flatMap(p -> ops.removeById(Person.class).oneEntity(p)) //
.doOnSuccess(p -> throwSimulateFailureException(p)); // remove has no result
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a removeByQuery then rolling back")
@Test
public void rollbackRemoveByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doMonoInTransaction(() -> {
attempts.incrementAndGet();
return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all()
.elementAt(0).map(p -> throwSimulateFailureException(p));
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a findByQuery then rolling back")
@Test
public void rollbackFindByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doMonoInTransaction(() -> {
attempts.incrementAndGet();
return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all()
.elementAt(0).map(p -> throwSimulateFailureException(p));
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
assertEquals(1, attempts.get());
}
@DisplayName("Forcing CAS mismatch causes a transaction retry")
@Test
public void casMismatchCausesRetry() {
Person person = blocking.insertById(Person.class).one(WalterWhite);
AtomicInteger attempts = new AtomicInteger();
// Needs to take place in a separate thread to bypass the ThreadLocalStorage checks
Thread forceCASMismatch = new Thread(() -> {
Person fetched = blocking.findById(Person.class).one(person.id());
blocking.replaceById(Person.class).one(fetched.withFirstName("Changed externally"));
});
doMonoInTransaction(() -> {
return ops.findById(Person.class).one(person.id()).flatMap(fetched -> Mono.defer(() -> {
if (attempts.incrementAndGet() == 1) {
forceCASMismatch.start();
try {
forceCASMismatch.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction"));
}));
});
Person fetched = blocking.findById(Person.class).one(person.id());
assertEquals("Changed by transaction", fetched.getFirstname());
assertEquals(2, attempts.get());
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 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 static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
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.data.couchbase.CouchbaseClientFactory;
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.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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 com.couchbase.client.core.error.transaction.AttemptExpiredException;
/**
* Tests for @Transactional methods, setting all the various options allowed by @Transactional.
*
* @author Graham Pople
*/
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(
classes = { TransactionsConfig.class, CouchbaseTransactionalOptionsIntegrationTests.PersonService.class })
public class CouchbaseTransactionalOptionsIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
@Autowired CouchbaseTemplate operations;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
}
@DisplayName("@Transactional(timeout = 2) will timeout at around 2 seconds")
@Test
public void timeout() {
long start = System.nanoTime();
Person person = operations.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> {
personService.timeout(person.id());
}, TransactionSystemUnambiguousException.class, AttemptExpiredException.class);
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() {
assertThrowsWithCause(() -> {
personService.unsupportedIsolation();
}, IllegalArgumentException.class);
}
@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
public <T> T doInTransaction(AtomicInteger tryCount, Function<CouchbaseOperations, T> callback) {
tryCount.incrementAndGet();
return callback.apply(ops);
}
@Transactional(timeout = 2)
public void timeout(String id) {
while (true) {
Person p = ops.findById(Person.class).one(id);
ops.replaceById(Person.class).one(p);
}
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void unsupportedIsolation() {}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void supportedIsolation() {}
}
}

View File

@@ -0,0 +1,354 @@
/*
* Copyright 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction;
import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
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.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.couchbase.client.core.error.transaction.RetryTransactionException;
/**
* Tests for the various propagation values allowed on @Transactional methods.
*
* @author Graham Pople
*/
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(
classes = { TransactionsConfig.class, CouchbaseTransactionalPropagationIntegrationTests.PersonService.class })
public class CouchbaseTransactionalPropagationIntegrationTests extends JavaIntegrationTests {
private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTransactionalPropagationIntegrationTests.class);
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
@Autowired CouchbaseTemplate operations;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
assertNotInTransaction();
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
@DisplayName("Call @Transactional(propagation = DEFAULT) - succeeds, creates a transaction")
@Test
public void callDefault() {
personService.propagationDefault(ops -> {
assertInTransaction();
});
}
@DisplayName("Call @Transactional(propagation = SUPPORTS) - fails as unsupported")
@Test
public void callSupports() {
assertThrowsWithCause(() -> personService.propagationSupports(ops -> {}), UnsupportedOperationException.class);
}
@DisplayName("Call @Transactional(propagation = MANDATORY) - fails as not in an active transaction")
@Test
public void callMandatory() {
assertThrowsWithCause(() -> personService.propagationMandatory(ops -> {}), IllegalTransactionStateException.class);
}
@DisplayName("Call @Transactional(propagation = REQUIRES_NEW) - fails as unsupported")
@Test
public void callRequiresNew() {
assertThrowsWithCause(() -> personService.propagationRequiresNew(ops -> {}), UnsupportedOperationException.class);
}
@DisplayName("Call @Transactional(propagation = NOT_SUPPORTED) - fails as unsupported")
@Test
public void callNotSupported() {
assertThrowsWithCause(() -> personService.propagationNotSupported(ops -> {}), UnsupportedOperationException.class);
}
@DisplayName("Call @Transactional(propagation = NEVER) - succeeds as not in a transaction, starts one")
@Test
public void callNever() {
personService.propagationNever(ops -> {
assertInTransaction();
});
}
@DisplayName("Call @Transactional(propagation = NESTED) - succeeds as not in an existing transaction, starts one")
@Test
public void callNested() {
personService.propagationNested(ops -> {
assertInTransaction();
});
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = DEFAULT) - succeeds, continues existing")
@Test
public void callDefaultThatCallsDefault() {
UUID id1 = UUID.randomUUID();
UUID id2 = UUID.randomUUID();
personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationDefault(ops2 -> {
ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper"));
assertInTransaction();
});
});
// Validate everything committed
assertNotNull(operations.findById(Person.class).one(id1.toString()));
assertNotNull(operations.findById(Person.class).one(id2.toString()));
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = REQUIRED) - succeeds, continues existing")
@Test
public void callDefaultThatCallsRequired() {
UUID id1 = UUID.randomUUID();
UUID id2 = UUID.randomUUID();
personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationRequired(ops2 -> {
ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper"));
assertInTransaction();
});
});
// Validate everything committed
assertNotNull(operations.findById(Person.class).one(id1.toString()));
assertNotNull(operations.findById(Person.class).one(id2.toString()));
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = MANDATORY) - succeeds, continues existing")
@Test
public void callDefaultThatCallsMandatory() {
UUID id1 = UUID.randomUUID();
UUID id2 = UUID.randomUUID();
personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationMandatory(ops2 -> {
ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper"));
assertInTransaction();
});
});
// Validate everything committed
assertNotNull(operations.findById(Person.class).one(id1.toString()));
assertNotNull(operations.findById(Person.class).one(id2.toString()));
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = REQUIRES_NEW) - fails as unsupported")
@Test
public void callDefaultThatCallsRequiresNew() {
UUID id1 = UUID.randomUUID();
assertThrowsWithCause(() -> personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationRequiresNew(ops2 -> {});
}), TransactionSystemUnambiguousException.class, UnsupportedOperationException.class);
// Validate everything rolled back
assertNull(operations.findById(Person.class).one(id1.toString()));
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = NOT_SUPPORTED) - fails as unsupported")
@Test
public void callDefaultThatCallsNotSupported() {
UUID id1 = UUID.randomUUID();
assertThrowsWithCause(() -> {
personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationNotSupported(ops2 -> {});
});
}, TransactionSystemUnambiguousException.class, UnsupportedOperationException.class);
// Validate everything rolled back
assertNull(operations.findById(Person.class).one(id1.toString()));
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = NEVER) - fails as in a transaction")
@Test
public void callDefaultThatCallsNever() {
UUID id1 = UUID.randomUUID();
assertThrowsWithCause(() -> {
personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationNever(ops2 -> {});
});
}, TransactionSystemUnambiguousException.class, IllegalTransactionStateException.class);
// Validate everything rolled back
assertNull(operations.findById(Person.class).one(id1.toString()));
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = NESTED) - fails as unsupported")
@Test
public void callDefaultThatCallsNested() {
UUID id1 = UUID.randomUUID();
assertThrowsWithCause(() -> {
personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationNested(ops2 -> {});
});
}, TransactionSystemUnambiguousException.class, UnsupportedOperationException.class);
// Validate everything rolled back
assertNull(operations.findById(Person.class).one(id1.toString()));
}
@DisplayName("Call @Transactional that calls @Transactional(propagation = DEFAULT) - check retries act correct")
@Test
public void callDefaultThatCallsDefaultRetries() {
UUID id1 = UUID.randomUUID();
UUID id2 = UUID.randomUUID();
AtomicInteger attempts = new AtomicInteger();
personService.propagationDefault(ops -> {
ops.insertById(Person.class).one(new Person(id1, "Ada", "Lovelace"));
personService.propagationDefault(ops2 -> {
ops2.insertById(Person.class).one(new Person(id2, "Grace", "Hopper"));
assertInTransaction();
if (attempts.incrementAndGet() < 3) {
throw new RetryTransactionException();
}
});
});
// Validate everything committed
assertNotNull(operations.findById(Person.class).one(id1.toString()));
assertNotNull(operations.findById(Person.class).one(id2.toString()));
assertEquals(3, attempts.get());
}
@Service
@Component
@EnableTransactionManagement
static class PersonService {
final CouchbaseOperations ops;
public PersonService(CouchbaseOperations ops) {
this.ops = ops;
}
@Transactional
public void propagationDefault(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationDefault");
if (callback != null)
callback.accept(ops);
}
@Transactional(propagation = Propagation.REQUIRED)
public void propagationRequired(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationRequired");
if (callback != null)
callback.accept(ops);
}
@Transactional(propagation = Propagation.MANDATORY)
public void propagationMandatory(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationMandatory");
if (callback != null)
callback.accept(ops);
}
@Transactional(propagation = Propagation.NESTED)
public void propagationNested(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationNever");
if (callback != null)
callback.accept(ops);
}
@Transactional(propagation = Propagation.SUPPORTS)
public void propagationSupports(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationSupports");
if (callback != null)
callback.accept(ops);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void propagationNotSupported(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationNotSupported");
if (callback != null)
callback.accept(ops);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void propagationRequiresNew(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationRequiresNew");
if (callback != null)
callback.accept(ops);
}
@Transactional(propagation = Propagation.NEVER)
public void propagationNever(@Nullable Consumer<CouchbaseOperations> callback) {
LOGGER.info("propagationNever");
if (callback != null)
callback.accept(ops);
}
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 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 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.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction;
import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserRepository;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
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;
/**
* Tests @Transactional with repository methods.
*
* @author Michael Reiche
*/
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(
classes = { TransactionsConfig.class, CouchbaseTransactionalRepositoryIntegrationTests.UserService.class })
public class CouchbaseTransactionalRepositoryIntegrationTests extends JavaIntegrationTests {
// intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated.
@Autowired UserRepository userRepo;
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired UserService userService;
@Autowired CouchbaseTemplate operations;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
assertNotInTransaction();
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
@Test
public void findByFirstname() {
operations.insertById(User.class).one(new User(UUID.randomUUID().toString(), "Ada", "Lovelace"));
List<User> users = userService.findByFirstname("Ada");
assertNotEquals(0, users.size());
}
@Test
public void save() {
String id = UUID.randomUUID().toString();
userService.run(repo -> {
assertInTransaction();
User user0 = repo.save(new User(id, "Ada", "Lovelace"));
assertInTransaction();
// read your own write
User user1 = operations.findById(User.class).one(id);
assertNotNull(user1);
assertInTransaction();
});
User user = operations.findById(User.class).one(id);
assertNotNull(user);
}
@DisplayName("Test that repo.save() is actually performed transactionally, by forcing a rollback")
@Test
public void saveRolledBack() {
String id = UUID.randomUUID().toString();
assertThrowsWithCause(() -> {
;
userService.run(repo -> {
User user = repo.save(new User(id, "Ada", "Lovelace"));
SimulateFailureException.throwEx("fail");
});
}, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
User user = operations.findById(User.class).one(id);
assertNull(user);
}
@Service
@Component
@EnableTransactionManagement
static class UserService {
@Autowired UserRepository userRepo;
@Transactional
public void run(Consumer<UserRepository> callback) {
callback.accept(userRepo);
}
@Transactional
public List<User> findByFirstname(String name) {
return userRepo.findByFirstname(name);
}
}
}

View File

@@ -0,0 +1,503 @@
/*
* Copyright 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 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.fail;
import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
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.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
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.RemoveResult;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.domain.PersonWithoutVersion;
import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
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 com.couchbase.client.core.error.transaction.AttemptExpiredException;
/**
* Tests for @Transactional, using template methods (findById etc.)
*
* @author Michael Reiche
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(
classes = { TransactionsConfig.class, CouchbaseTransactionalTemplateIntegrationTests.PersonService.class })
public class CouchbaseTransactionalTemplateIntegrationTests extends JavaIntegrationTests {
// intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated.
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
@Autowired CouchbaseTemplate operations;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
// Skip this as we just one to track TransactionContext
List<RemoveResult> pr = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<Person> p = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<RemoveResult> pwovr = operations.removeByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).all();
List<PersonWithoutVersion> pwov = operations.findByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS)
.all();
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
@DisplayName("A basic golden path insert should succeed")
@Test
public void committedInsert() {
AtomicInteger tryCount = new AtomicInteger(0);
Person inserted = personService.doInTransaction(tryCount, (ops) -> {
return ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
});
Person fetched = operations.findById(Person.class).one(inserted.id());
assertEquals(inserted.getFirstname(), fetched.getFirstname());
assertEquals(1, tryCount.get());
}
@DisplayName("A basic golden path replace should succeed")
@Test
public void committedReplace() {
AtomicInteger tryCount = new AtomicInteger();
Person person = operations.insertById(Person.class).one(WalterWhite);
personService.fetchAndReplace(person.id(), tryCount, (p) -> {
p.setFirstname("changed");
return p;
});
Person fetched = operations.findById(Person.class).one(person.id());
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 = operations.insertById(Person.class).one(WalterWhite);
personService.fetchAndRemove(person.id(), tryCount);
Person fetched = operations.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, tryCount.get());
}
@DisplayName("A basic golden path removeByQuery should succeed")
@Test
public void committedRemoveByQuery() {
AtomicInteger tryCount = new AtomicInteger();
Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname());
List<RemoveResult> removed = personService.doInTransaction(tryCount, ops -> {
return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
});
Person fetched = operations.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, tryCount.get());
assertEquals(1, removed.size());
}
@DisplayName("A basic golden path findByQuery should succeed (though we don't know for sure it executed transactionally)")
@Test
public void committedFindByQuery() {
AtomicInteger tryCount = new AtomicInteger(0);
Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname());
List<Person> found = personService.doInTransaction(tryCount, ops -> {
return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
});
assertEquals(1, found.size());
}
@DisplayName("Basic test of doing an insert then rolling back")
@Test
public void rollbackInsert() {
AtomicInteger tryCount = new AtomicInteger();
AtomicReference<String> id = new AtomicReference<>();
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, (ops) -> {
ops.insertById(Person.class).one(WalterWhite);
id.set(WalterWhite.id());
throw new SimulateFailureException();
});
}, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
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 = operations.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, (ops) -> {
Person p = ops.findById(Person.class).one(person.id());
ops.replaceById(Person.class).one(p.withFirstName("changed"));
throw new SimulateFailureException();
});
}, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = operations.findById(Person.class).one(person.id());
assertEquals(person.getFirstname(), 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 = operations.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, (ops) -> {
Person p = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).oneEntity(p);
throw new SimulateFailureException();
});
}, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = operations.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, tryCount.get());
}
@DisplayName("Basic test of doing a removeByQuery then rolling back")
@Test
public void rollbackRemoveByQuery() {
AtomicInteger tryCount = new AtomicInteger();
Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname());
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, ops -> {
ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
throw new SimulateFailureException();
});
}, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = operations.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, tryCount.get());
}
@DisplayName("Basic test of doing a findByQuery then rolling back")
@Test
public void rollbackFindByQuery() {
AtomicInteger tryCount = new AtomicInteger();
Person person = operations.insertById(Person.class).one(WalterWhite.withIdFirstname());
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, ops -> {
ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
throw new SimulateFailureException();
});
}, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
assertEquals(1, tryCount.get());
}
@Test
public void shouldRollbackAfterException() {
assertThrowsWithCause(() -> {
personService.insertThenThrow();
}, TransactionSystemUnambiguousException.class, SimulateFailureException.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 commitShouldPersistTxEntries() {
Person p = personService.declarativeSavePerson(WalterWhite);
Long count = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count();
assertEquals(1, count, "should have saved and found 1");
}
@Test
public void concurrentTxns() {
Runnable r = () -> {
Thread t = Thread.currentThread();
System.out.printf("Started thread %d %s%n", t.getId(), t.getName());
Person p = personService.declarativeSavePersonWithThread(WalterWhite, t);
System.out.printf("Finished thread %d %s%n", t.getId(), t.getName());
};
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 50; i++) { // somewhere between 50-80 it starts to hang
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(); // interrupted
}
});
}
@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 = operations.insertById(Person.class).one(WalterWhite);
Person refetched = operations.findById(Person.class).one(person.id());
operations.replaceById(Person.class).one(refetched);
assertNotEquals(person.getVersion(), refetched.getVersion());
AtomicInteger tryCount = new AtomicInteger(0);
assertThrowsWithCause(() -> personService.replace(person, tryCount), TransactionSystemUnambiguousException.class,
AttemptExpiredException.class);
}
@DisplayName("Entity must have CAS field during replace")
@Test
public void replaceEntityWithoutCas() {
PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White");
operations.insertById(PersonWithoutVersion.class).one(person);
assertThrowsWithCause(() -> personService.replaceEntityWithoutVersion(person.id()),
TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have non-zero CAS during replace")
@Test
public void replaceEntityWithCasZero() {
Person person = operations.insertById(Person.class).one(WalterWhite);
// switchedPerson here will have CAS=0, which will fail
Person switchedPerson = new Person("Dave", "Reynolds");
AtomicInteger tryCount = new AtomicInteger(0);
assertThrowsWithCause(() -> personService.replacePerson(switchedPerson, tryCount),
TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have CAS field during remove")
@Test
public void removeEntityWithoutCas() {
PersonWithoutVersion person = new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White");
operations.insertById(PersonWithoutVersion.class).one(person);
assertThrowsWithCause(() -> personService.removeEntityWithoutVersion(person.id()),
TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS")
@Test
public void removeEntityById() {
AtomicInteger tryCount = new AtomicInteger();
Person person = operations.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, (ops) -> {
Person p = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).one(p.id());
return p;
});
}, TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@Service
@Component
@EnableTransactionManagement
static class PersonService {
final CouchbaseOperations personOperations;
final ReactiveCouchbaseOperations personOperationsRx;
public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx) {
personOperations = ops;
personOperationsRx = opsRx;
}
@Transactional
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
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
public void insertThenThrow() {
assertInAnnotationTransaction(true);
Person person = personOperations.insertById(Person.class).one(new Person("Walter", "White"));
SimulateFailureException.throwEx();
}
@Autowired CouchbaseCallbackTransactionManager callbackTm;
/**
* to execute while ThreadReplaceloop() is running should force a retry
*
* @param person
* @return
*/
@Transactional
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
public void replaceEntityWithoutVersion(String id) {
PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).one(id);
personOperations.replaceById(PersonWithoutVersion.class).one(fetched);
}
@Transactional
public void removeEntityWithoutVersion(String id) {
PersonWithoutVersion fetched = personOperations.findById(PersonWithoutVersion.class).one(id);
personOperations.removeById(PersonWithoutVersion.class).oneEntity(fetched);
}
@Transactional
public Person declarativeFindReplaceTwicePersonCallback(Person person, AtomicInteger tryCount) {
assertInAnnotationTransaction(true);
System.err.println("declarativeFindReplacePersonCallback try: " + tryCount.incrementAndGet());
Person p = personOperations.findById(Person.class).one(person.id());
Person pUpdated = personOperations.replaceById(Person.class).one(p);
return personOperations.replaceById(Person.class).one(pUpdated);
}
@Transactional(timeout = 2)
public Person replace(Person person, AtomicInteger tryCount) {
assertInAnnotationTransaction(true);
tryCount.incrementAndGet();
return personOperations.replaceById(Person.class).one(person);
}
@Transactional
public Person fetchAndReplace(String id, AtomicInteger tryCount, Function<Person, Person> 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
public <T> T doInTransaction(AtomicInteger tryCount, Function<CouchbaseOperations, T> callback) {
assertInAnnotationTransaction(true);
tryCount.incrementAndGet();
return callback.apply(personOperations);
}
@Transactional
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);
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 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 static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
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.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
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 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;
/**
* Tests for @Transactional methods, where parameters/options are being set that aren't support in a transaction. These
* will be rejected at runtime.
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(classes = { TransactionsConfig.class,
CouchbaseTransactionalUnsettableParametersIntegrationTests.PersonService.class })
public class CouchbaseTransactionalUnsettableParametersIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
Person WalterWhite;
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
}
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
void test(Consumer<CouchbaseOperations> r) {
AtomicInteger tryCount = new AtomicInteger(0);
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, (ops) -> {
r.accept(ops);
return null;
});
}, TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
assertEquals(1, tryCount.get());
}
@DisplayName("Using insertById().withDurability - the PersistTo overload - in a transaction is rejected at runtime")
@Test
public void insertWithDurability() {
test((ops) -> {
ops.insertById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).one(WalterWhite);
});
}
@DisplayName("Using insertById().withExpiry in a transaction is rejected at runtime")
@Test
public void insertWithExpiry() {
test((ops) -> {
ops.insertById(Person.class).withExpiry(Duration.ofSeconds(3)).one(WalterWhite);
});
}
@DisplayName("Using insertById().withOptions in a transaction is rejected at runtime")
@Test
public void insertWithOptions() {
test((ops) -> {
ops.insertById(Person.class).withOptions(InsertOptions.insertOptions()).one(WalterWhite);
});
}
@DisplayName("Using replaceById().withDurability - the PersistTo overload - in a transaction is rejected at runtime")
@Test
public void replaceWithDurability() {
test((ops) -> {
ops.replaceById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).one(WalterWhite);
});
}
@DisplayName("Using replaceById().withExpiry in a transaction is rejected at runtime")
@Test
public void replaceWithExpiry() {
test((ops) -> {
ops.replaceById(Person.class).withExpiry(Duration.ofSeconds(3)).one(WalterWhite);
});
}
@DisplayName("Using replaceById().withOptions in a transaction is rejected at runtime")
@Test
public void replaceWithOptions() {
test((ops) -> {
ops.replaceById(Person.class).withOptions(ReplaceOptions.replaceOptions()).one(WalterWhite);
});
}
@DisplayName("Using removeById().withDurability - the PersistTo overload - in a transaction is rejected at runtime")
@Test
public void removeWithDurability() {
test((ops) -> {
ops.removeById(Person.class).withDurability(PersistTo.ONE, ReplicateTo.ONE).oneEntity(WalterWhite);
});
}
@DisplayName("Using removeById().withOptions in a transaction is rejected at runtime")
@Test
public void removeWithOptions() {
test((ops) -> {
ops.removeById(Person.class).withOptions(RemoveOptions.removeOptions()).oneEntity(WalterWhite);
});
}
@Service
@Component
@EnableTransactionManagement
static class PersonService {
final CouchbaseOperations personOperations;
public PersonService(CouchbaseOperations ops) {
personOperations = ops;
}
@Transactional
public <T> T doInTransaction(AtomicInteger tryCount, Function<CouchbaseOperations, T> callback) {
tryCount.incrementAndGet();
return callback.apply(personOperations);
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 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 org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager;
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.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* We do not support direct use of the PlatformTransactionManager.
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
public class DirectPlatformTransactionManagerIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Test
public void directUseAlwaysFails() {
PlatformTransactionManager ptm = new CouchbaseCallbackTransactionManager(couchbaseClientFactory);
assertThrowsWithCause(() -> {
TransactionDefinition def = new DefaultTransactionDefinition();
ptm.getTransaction(def);
}, UnsupportedOperationException.class);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 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 java.util.UUID;
/**
* ObjectId for Transaction tests
*
* @author Michael Reiche
*/
public class ObjectId {
public ObjectId() {
id = UUID.randomUUID().toString();
}
String id;
public String toString() {
return id.toString();
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static org.springframework.data.couchbase.util.JavaIntegrationTests.throwSimulateFailureException;
import static org.springframework.data.couchbase.util.Util.assertInAnnotationTransaction;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.reactive.TransactionalOperator;
/**
* PersonService for tests
*
* @author Michael Reiche
*/
@Service
@Component
@EnableTransactionManagement
class PersonService {
final CouchbaseOperations personOperations;
final ReactiveCouchbaseOperations personOperationsRx;
final TransactionalOperator transactionalOperator;
public PersonService(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx,
TransactionalOperator transactionalOperator) {
personOperations = ops;
personOperationsRx = opsRx;
this.transactionalOperator = transactionalOperator;
}
public Person savePersonErrors(Person person) {
assertInAnnotationTransaction(false);
return personOperationsRx.insertById(Person.class).one(person)//
.<Person> flatMap(it -> Mono.error(new SimulateFailureException()))//
.as(transactionalOperator::transactional).block();
}
public Person savePerson(Person person) {
assertInAnnotationTransaction(false);
return personOperationsRx.insertById(Person.class).one(person)//
.as(transactionalOperator::transactional).block();
}
public Long countDuringTx(Person person) {
assertInAnnotationTransaction(false);
return personOperationsRx.insertById(Person.class).one(person)//
.then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count())
.as(transactionalOperator::transactional).block();
}
public List<CouchbasePersonTransactionIntegrationTests.EventLog> saveWithLogs(Person person) {
assertInAnnotationTransaction(false);
return Flux
.merge(
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeConvert")),
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterConvert")),
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeInsert")),
personOperationsRx.insertById(Person.class).one(person),
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) //
.thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.withConsistency(REQUEST_PLUS).all()) //
.as(transactionalOperator::transactional).collectList().block();
}
public List<CouchbasePersonTransactionIntegrationTests.EventLog> saveWithErrorLogs(Person person) {
assertInAnnotationTransaction(false);
return Flux
.merge(
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeConvert")),
//
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterConvert")),
//
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "beforeInsert")),
//
personOperationsRx.insertById(Person.class).one(person),
//
personOperationsRx.insertById(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.one(new CouchbasePersonTransactionIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) //
.thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionIntegrationTests.EventLog.class)
.withConsistency(REQUEST_PLUS).all()) //
.<CouchbasePersonTransactionIntegrationTests.EventLog> flatMap(it -> Mono.error(new SimulateFailureException()))
.as(transactionalOperator::transactional).collectList().block();
}
// org.springframework.beans.factory.NoUniqueBeanDefinitionException:
// No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single
// matching bean but found 2: reactiveCouchbaseTransactionManager,couchbaseTransactionManager
@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER)
public Person declarativeSavePerson(Person person) {
assertInAnnotationTransaction(true);
return personOperations.insertById(Person.class).one(person);
}
@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER)
public Person declarativeSavePersonErrors(Person person) {
assertInAnnotationTransaction(true);
Person p = personOperations.insertById(Person.class).one(person); //
SimulateFailureException.throwEx();
return p;
}
/**
* to execute while ThreadReplaceloop() is running should force a retry
*
* @param person
* @return
*/
@Transactional(transactionManager = BeanNames.COUCHBASE_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.id());
ReplaceLoopThread.updateOutOfTransaction(personOperations, person, tryCount.get());
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)
// must use transactionalOperator
public Mono<Person> declarativeFindReplacePersonReactive(Person person, AtomicInteger tryCount) {
// assertInAnnotationTransaction(true);
return personOperationsRx.findById(Person.class).one(person.id())
.map((p) -> ReplaceLoopThread.updateOutOfTransaction(personOperations, p, tryCount.incrementAndGet()))
.flatMap(p -> personOperationsRx.replaceById(Person.class).one(p.withFirstName(person.getFirstname())))
.as(transactionalOperator::transactional);
}
/**
* @param person
* @return
*/
@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER)
public Person declarativeFindReplacePerson(Person person, AtomicInteger tryCount) {
assertInAnnotationTransaction(true);
System.err.println("declarativeFindReplacePerson try: " + tryCount.incrementAndGet());
Person p = personOperations.findById(Person.class).one(person.getId().toString());
ReplaceLoopThread.updateOutOfTransaction(personOperations, person, tryCount.get());
return personOperations.replaceById(Person.class).one(p.withFirstName(person.getFirstname()));
}
// @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER)
// must use transactionalOperator
public Mono<Person> declarativeSavePersonReactive(Person person) {
// assertInAnnotationTransaction(true);
return personOperationsRx.insertById(Person.class).one(person).as(transactionalOperator::transactional);
}
// @Transactional(transactionManager = BeanNames.REACTIVE_COUCHBASE_TRANSACTION_MANAGER)
// must use transactionalOperator
public Mono<Person> declarativeSavePersonErrorsReactive(Person person) {
// assertInAnnotationTransaction(true);
return personOperationsRx.insertById(Person.class).one(person).map((pp) -> throwSimulateFailureException(pp))
.as(transactionalOperator::transactional); //
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.reactive.TransactionalOperator;
/**
* reactive PersonService for tests
*
* @author Michael Reiche
*/
class PersonServiceReactive {
final ReactiveCouchbaseOperations personOperationsRx;
final CouchbaseOperations personOperations;
final TransactionalOperator transactionalOperator;
public PersonServiceReactive(CouchbaseOperations ops, ReactiveCouchbaseOperations opsRx,
TransactionalOperator transactionalOperator) {
this.personOperations = ops;
this.personOperationsRx = opsRx;
this.transactionalOperator = transactionalOperator;
return;
}
public Mono<Person> savePersonErrors(Person person) {
return personOperationsRx.insertById(Person.class).one(person) //
.<Person> flatMap(it -> Mono.error(new SimulateFailureException())) //
.as(transactionalOperator::transactional);
}
public Mono<Person> savePerson(Person person) {
return personOperationsRx.insertById(Person.class).one(person) //
.flatMap(Mono::just) //
.as(transactionalOperator::transactional);
}
public Mono<Long> countDuringTx(Person person) {
return personOperationsRx.save(person) //
.then(personOperationsRx.findByQuery(Person.class).withConsistency(REQUEST_PLUS).count()) //
.as(transactionalOperator::transactional);
}
public Flux<CouchbasePersonTransactionReactiveIntegrationTests.EventLog> saveWithLogs(Person person) {
return Flux
.merge(
personOperationsRx.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(
new ObjectId().toString(), "beforeConvert")),
personOperationsRx
.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterConvert")),
personOperationsRx
.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "beforeInsert")),
personOperationsRx.save(person),
personOperationsRx
.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) //
.thenMany(personOperationsRx.findByQuery(CouchbasePersonTransactionReactiveIntegrationTests.EventLog.class)
.withConsistency(REQUEST_PLUS).all()) //
.as(transactionalOperator::transactional);
}
public Flux<Void> saveWithErrorLogs(Person person) {
return Flux
.merge(
personOperationsRx
.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "beforeConvert")),
personOperationsRx
.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterConvert")),
personOperationsRx
.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "beforeInsert")),
personOperationsRx.save(person),
personOperationsRx
.save(new CouchbasePersonTransactionReactiveIntegrationTests.EventLog(new ObjectId(), "afterInsert"))) //
.<Void> flatMap(it -> Mono.error(new SimulateFailureException())) //
.as(transactionalOperator::transactional);
}
// @Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER)
public Flux<Person> declarativeSavePerson(Person person) {
return transactionalOperator.execute(reactiveTransaction -> personOperationsRx.save(person));
}
@Transactional(transactionManager = BeanNames.COUCHBASE_TRANSACTION_MANAGER)
public Flux<Person> declarativeSavePersonErrors(Person person) {
Person p = personOperations.insertById(Person.class).one(person);
Person pp = personOperations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all().get(0);
SimulateFailureException.throwEx(); // so the following lines is not flagged as unreachable
return Flux.just(p);
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright 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 static org.junit.jupiter.api.Assertions.assertEquals;
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.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
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.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
import org.springframework.data.couchbase.core.TransactionalSupport;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
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;
/**
* Tests for reactive @Transactional, using the CouchbaseTransactionInterceptor.
*
* @author Graham Pople
*/
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(
classes = { TransactionsConfig.class, ReactiveTransactionalTemplateIntegrationTests.PersonService.class })
public class ReactiveTransactionalTemplateIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
@Autowired CouchbaseTemplate blocking;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
assertNotInTransaction();
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
@DisplayName("A basic golden path insert should succeed")
@Test
public void committedInsert() {
AtomicInteger tryCount = new AtomicInteger(0);
personService.doInTransaction(tryCount, (ops) -> {
return Mono.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite);
});
}).block();
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertEquals(WalterWhite.getFirstname(), fetched.getFirstname());
assertEquals(1, tryCount.get());
}
@DisplayName("Basic test of doing an insert then rolling back")
@Test
public void rollbackInsert() {
AtomicInteger tryCount = new AtomicInteger();
assertThrowsWithCause(() -> {
personService.doInTransaction(tryCount, (ops) -> {
return Mono.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite).then(Mono.error(new SimulateFailureException()));
});
}).block();
}, TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertNull(fetched);
assertEquals(1, tryCount.get());
}
@DisplayName("Forcing CAS mismatch causes a transaction retry")
@Test
public void casMismatchCausesRetry() {
Person person = blocking.insertById(Person.class).one(WalterWhite);
AtomicInteger attempts = new AtomicInteger();
personService.doInTransaction(attempts, ops -> {
return ops.findById(Person.class).one(person.id()).flatMap(fetched -> Mono.fromRunnable(() -> {
ReplaceLoopThread.updateOutOfTransaction(blocking, person.withFirstName("ChangedExternally"), attempts.get());
}).then(ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction"))));
}).block();
Person fetched = blocking.findById(Person.class).one(person.getId().toString());
assertEquals("Changed by transaction", fetched.getFirstname());
assertEquals(2, attempts.get());
}
@Test
public void returnMono() {
AtomicInteger tryCount = new AtomicInteger(0);
Person fromLambda = personService.doInTransactionReturningMono(tryCount, (ops) -> {
return Mono.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite).log("source");
}).log("returnMono test");
}).block();
assertNotNull(fromLambda);
assertEquals(WalterWhite.getFirstname(), fromLambda.getFirstname());
}
@Test
public void returnFlux() {
AtomicInteger tryCount = new AtomicInteger(0);
List<Integer> fromLambda = personService.doInTransactionReturningFlux(tryCount, (ops) -> {
return Flux.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite)
.thenMany(Flux.fromIterable(Arrays.asList(1, 2, 3)).log("1"));
});
}).collectList().block();
assertEquals(3, fromLambda.size());
}
@Service
@Component
@EnableTransactionManagement
static class PersonService {
final ReactiveCouchbaseOperations ops;
public PersonService(ReactiveCouchbaseOperations ops) {
this.ops = ops;
}
@Transactional
public Mono<Void> doInTransaction(AtomicInteger tryCount, Function<ReactiveCouchbaseOperations, Mono<?>> callback) {
return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(stat -> {
assertTrue(stat.isPresent(), "Not in transaction");
tryCount.incrementAndGet();
return callback.apply(ops).then();
});
}
@Transactional
public <T> Mono<T> doInTransactionReturningMono(AtomicInteger tryCount,
Function<ReactiveCouchbaseOperations, Mono<T>> callback) {
return Mono.defer(() -> {
tryCount.incrementAndGet();
return callback.apply(ops);
});
}
@Transactional
public <T> Flux<T> doInTransactionReturningFlux(AtomicInteger tryCount,
Function<ReactiveCouchbaseOperations, Flux<T>> callback) {
return Flux.defer(() -> {
tryCount.incrementAndGet();
return callback.apply(ops);
});
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 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 java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Assert;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
/**
* For testing transactions
*
* @author Michael Reiche
*/
public class ReplaceLoopThread extends Thread {
private final CouchbaseOperations couchbaseOperations;
AtomicBoolean stop = new AtomicBoolean(false);
UUID id;
int maxIterations = 100;
public ReplaceLoopThread(CouchbaseOperations couchbaseOperations, UUID id, int... iterations) {
Assert.assertNotNull("couchbaseOperations cannot be null", couchbaseOperations);
this.couchbaseOperations = couchbaseOperations;
this.id = id;
if (iterations != null && iterations.length == 1) {
this.maxIterations = iterations[0];
}
}
public void run() {
for (int i = 0; i < maxIterations && !stop.get(); i++) {
JavaIntegrationTests.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
Person fetched = couchbaseOperations.findById(Person.class).one(id.toString());
couchbaseOperations.replaceById(Person.class).one(fetched.withFirstName("Changed externally"));
System.out.println("********** replace thread: " + i + " success");
} catch (Exception e) {
System.out.println("********** replace thread: " + i + " " + e.getClass().getName());
e.printStackTrace();
}
}
}
public void setStopFlag() {
stop.set(true);
}
public static Person updateOutOfTransaction(CouchbaseOperations couchbaseOperations, Person pp, int tryCount) {
System.err.println("updateOutOfTransaction: " + tryCount);
if (tryCount < 1) {
throw new RuntimeException("increment before calling updateOutOfTransactions");
}
if (tryCount > 1) {
return pp;
}
ReplaceLoopThread t = new ReplaceLoopThread(couchbaseOperations, pp.getId(), 1);
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
return pp;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 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;
/**
* A recognizable exception for testing transactions
*
* @author Michael Reiche
*/
public 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);
}
}

View File

@@ -0,0 +1,377 @@
/*
* Copyright 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 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
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.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.RemoveResult;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.domain.PersonWithoutVersion;
import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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.IllegalTransactionStateException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Tests for Spring's TransactionTemplate, used CouchbaseCallbackTransactionManager, using template methods (findById
* etc.)
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
public class TransactionTemplateIntegrationTests extends JavaIntegrationTests {
TransactionTemplate template;
@Autowired CouchbaseCallbackTransactionManager transactionManager;
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired CouchbaseTemplate ops;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
assertNotInTransaction();
List<RemoveResult> rp0 = ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<RemoveResult> rp1 = ops.removeByQuery(PersonWithoutVersion.class).withConsistency(REQUEST_PLUS).all();
template = new TransactionTemplate(transactionManager);
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
static class RunResult {
public final int attempts;
public RunResult(int attempts) {
this.attempts = attempts;
}
}
private RunResult doInTransaction(Consumer<TransactionStatus> lambda) {
AtomicInteger tryCount = new AtomicInteger();
template.executeWithoutResult(status -> {
TransactionTestUtil.assertInTransaction();
assertFalse(status.hasSavepoint());
assertFalse(status.isRollbackOnly());
assertFalse(status.isCompleted());
assertTrue(status.isNewTransaction());
tryCount.incrementAndGet();
lambda.accept(status);
});
TransactionTestUtil.assertNotInTransaction();
return new RunResult(tryCount.get());
}
@DisplayName("A basic golden path insert should succeed")
@Test
public void committedInsert() {
RunResult rr = doInTransaction(status -> {
ops.insertById(Person.class).one(WalterWhite);
});
Person fetched = ops.findById(Person.class).one(WalterWhite.id());
assertEquals(WalterWhite.getFirstname(), fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path replace should succeed")
@Test
public void committedReplace() {
Person person = ops.insertById(Person.class).one(WalterWhite);
RunResult rr = doInTransaction(status -> {
Person p = ops.findById(Person.class).one(person.id());
ops.replaceById(Person.class).one(p.withFirstName("changed"));
});
Person fetched = ops.findById(Person.class).one(person.id());
assertEquals("changed", fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path remove should succeed")
@Test
public void committedRemove() {
Person person = ops.insertById(Person.class).one(WalterWhite);
RunResult rr = doInTransaction(status -> {
Person fetched = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).oneEntity(fetched);
});
Person fetched = ops.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path removeByQuery should succeed")
@Test
public void committedRemoveByQuery() {
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doInTransaction(status -> {
List<RemoveResult> removed = ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS)
.matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
assertEquals(1, removed.size());
});
Person fetched = ops.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path findByQuery should succeed")
@Test
public void committedFindByQuery() {
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doInTransaction(status -> {
List<Person> found = ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS)
.matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
assertEquals(1, found.size());
});
assertEquals(1, rr.attempts);
}
@DisplayName("Basic test of doing an insert then rolling back")
@Test
public void rollbackInsert() {
AtomicInteger attempts = new AtomicInteger();
assertThrowsWithCause(() -> doInTransaction(status -> {
attempts.incrementAndGet();
Person person = ops.insertById(Person.class).one(WalterWhite);
throw new SimulateFailureException();
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(WalterWhite.id());
assertNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a replace then rolling back")
@Test
public void rollbackReplace() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(status -> {
attempts.incrementAndGet();
Person p = ops.findById(Person.class).one(person.id());
p.setFirstname("changed");
ops.replaceById(Person.class).one(p);
throw new SimulateFailureException();
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(person.id());
assertEquals(person.getFirstname(), fetched.getFirstname());
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a remove then rolling back")
@Test
public void rollbackRemove() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(status -> {
attempts.incrementAndGet();
Person p = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).oneEntity(p);
throw new SimulateFailureException();
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a removeByQuery then rolling back")
@Test
public void rollbackRemoveByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
assertThrowsWithCause(() -> doInTransaction(status -> {
attempts.incrementAndGet();
ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS)
.matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
throw new SimulateFailureException();
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a findByQuery then rolling back")
@Test
public void rollbackFindByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
assertThrowsWithCause(() -> doInTransaction(status -> {
attempts.incrementAndGet();
ops.findByQuery(Person.class).withConsistency(REQUEST_PLUS)
.matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
throw new SimulateFailureException();
}), TransactionSystemUnambiguousException.class, SimulateFailureException.class);
assertEquals(1, attempts.get());
}
@DisplayName("Entity must have CAS field during replace")
@Test
public void replaceEntityWithoutCas() {
PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class)
.one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"));
assertThrowsWithCause(() -> {
doInTransaction(status -> {
PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id());
ops.replaceById(PersonWithoutVersion.class).one(fetched);
});
}, TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have non-zero CAS during replace")
@Test
public void replaceEntityWithCasZero() {
Person person = ops.insertById(Person.class).one(WalterWhite);
// switchedPerson here will have CAS=0, which will fail
Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds");
assertThrowsWithCause(() -> doInTransaction(status -> {
ops.replaceById(Person.class).one(switchedPerson);
}), TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have CAS field during remove")
@Test
public void removeEntityWithoutCas() {
PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class)
.one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"));
assertThrowsWithCause(() -> doInTransaction(status -> {
PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id());
ops.removeById(PersonWithoutVersion.class).oneEntity(fetched);
}), TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS")
@Test
public void removeEntityById() {
Person person = ops.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(status -> {
Person p = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).one(p.id());
}), TransactionSystemUnambiguousException.class, IllegalArgumentException.class);
}
@DisplayName("setRollbackOnly should cause a rollback")
@Test
public void setRollbackOnly() {
assertThrowsWithCause(() -> doInTransaction(status -> {
status.setRollbackOnly();
Person person = ops.insertById(Person.class).one(WalterWhite);
}), TransactionSystemUnambiguousException.class);
Person fetched = ops.findById(Person.class).one(WalterWhite.id());
assertNull(fetched);
}
@DisplayName("Setting an unsupported isolation level should fail")
@Test
public void unsupportedIsolationLevel() {
template.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
assertThrowsWithCause(() -> doInTransaction(status -> {}), IllegalArgumentException.class);
}
@DisplayName("Setting PROPAGATION_MANDATORY should fail, as not in a transaction")
@Test
public void propagationMandatoryOutsideTransaction() {
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);
assertThrowsWithCause(() -> doInTransaction(status -> {}), IllegalTransactionStateException.class);
}
@Test
public void nestedTransactionTemplates() {
TransactionTemplate template2 = new TransactionTemplate(transactionManager);
template2.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);
template.executeWithoutResult(status -> {
template2.executeWithoutResult(status2 -> {
Person person = ops.insertById(Person.class).one(WalterWhite);
});
});
Person fetched = ops.findById(Person.class).one(WalterWhite.id());
assertEquals("Walter", fetched.getFirstname());
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 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 java.time.Duration;
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.env.ClusterEnvironment;
/**
* For testing transactions
*
* @author Michael Reiche
*/
@Configuration
@EnableCouchbaseRepositories("org.springframework.data.couchbase")
@EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase")
@EnableTransactionManagement
public class TransactionsConfig 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 void configureEnvironment(ClusterEnvironment.Builder builder) {
// twenty minutes for debugging in the debugger.
builder.transactionsConfig(
com.couchbase.client.java.transactions.config.TransactionsConfig.builder().timeout(Duration.ofMinutes(20)));
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 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.sdk;
import static org.junit.jupiter.api.Assertions.assertEquals;
import reactor.core.publisher.Mono;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
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.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.transactions.TransactionsConfig;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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 com.couchbase.client.java.transactions.error.TransactionFailedException;
/**
* Tests for regular reactive SDK transactions, where Spring operations that aren't supported in a transaction are being
* used. They should be prevented at runtime.
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(classes = { TransactionsConfig.class,
SDKReactiveTransactionsNonAllowableOperationsIntegrationTests.PersonService.class })
public class SDKReactiveTransactionsNonAllowableOperationsIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
}
void test(Function<ReactiveCouchbaseOperations, Mono<?>> r) {
AtomicInteger tryCount = new AtomicInteger(0);
assertThrowsWithCause(() -> {
couchbaseClientFactory.getCluster().reactive().transactions().run(ignored -> {
return personService.doInService(tryCount, (ops) -> {
return r.apply(ops);
});
}).block();
}, TransactionFailedException.class, IllegalArgumentException.class);
assertEquals(1, tryCount.get());
}
@DisplayName("Using existsById() in a transaction is rejected at runtime")
@Test
public void existsById() {
test((ops) -> {
return ops.existsById(Person.class).one(WalterWhite.id());
});
}
@DisplayName("Using findByAnalytics() in a transaction is rejected at runtime")
@Test
public void findByAnalytics() {
test((ops) -> {
return ops.findByAnalytics(Person.class).one();
});
}
@DisplayName("Using findFromReplicasById() in a transaction is rejected at runtime")
@Test
public void findFromReplicasById() {
test((ops) -> {
return ops.findFromReplicasById(Person.class).any(WalterWhite.id());
});
}
@DisplayName("Using upsertById() in a transaction is rejected at runtime")
@Test
public void upsertById() {
test((ops) -> {
return ops.upsertById(Person.class).one(WalterWhite);
});
}
// This is intentionally not a @Transactional service
@Service
@Component
static class PersonService {
final ReactiveCouchbaseOperations personOperations;
public PersonService(ReactiveCouchbaseOperations ops) {
personOperations = ops;
}
public <T> Mono<T> doInService(AtomicInteger tryCount, Function<ReactiveCouchbaseOperations, Mono<T>> callback) {
tryCount.incrementAndGet();
return callback.apply(personOperations);
}
}
}

View File

@@ -0,0 +1,273 @@
/*
* Copyright 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.sdk;
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.springframework.data.couchbase.transactions.ReplaceLoopThread.updateOutOfTransaction;
import reactor.core.publisher.Mono;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
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.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.couchbase.CouchbaseClientFactory;
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.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.transactions.ReplaceLoopThread;
import org.springframework.data.couchbase.transactions.SimulateFailureException;
import org.springframework.data.couchbase.transactions.TransactionsConfig;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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.java.transactions.TransactionResult;
import com.couchbase.client.java.transactions.error.TransactionFailedException;
/**
* Tests for ReactiveTransactionsWrapper, moved from CouchbasePersonTransactionIntegrationTests. Now
* ReactiveTransactionsWrapper is removed, these are testing the same operations inside a regular SDK transaction.
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
public class SDKReactiveTransactionsPersonIntegrationTests extends JavaIntegrationTests {
// intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated.
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired ReactivePersonRepository rxRepo;
@Autowired PersonRepository repo;
@Autowired CouchbaseTemplate cbTmpl;
@Autowired ReactiveCouchbaseTemplate rxCBTmpl;
@Autowired CouchbaseTemplate operations;
String sName = "_default";
String cName = "_default";
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@AfterAll
public static void afterAll() {
callSuperAfterAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
List<RemoveResult> rp0 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<RemoveResult> rp1 = operations.removeByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName)
.inCollection(cName).all();
List<Person> p0 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).all();
List<Person> p1 = operations.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName)
.inCollection(cName).all();
}
@AfterEach
public void afterEachTest() {
TransactionTestUtil.assertNotInTransaction();
}
@Test
// need to fix this to make it deliberately have the CasMismatch by synchronization.
// And to *not* do any out-of-tx updates after the tx update has succeeded.
// And to have the tx update to a different name than the out-of-tx update
public void wrapperReplaceWithCasConflictResolvedViaRetryReactive() {
AtomicInteger tryCount = new AtomicInteger();
Person person = cbTmpl.insertById(Person.class).one(WalterWhite);
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions()
.run(ctx -> rxCBTmpl.findById(Person.class).one(person.id()) //
.map((pp) -> updateOutOfTransaction(cbTmpl, pp, tryCount.incrementAndGet()))
.flatMap(ppp -> rxCBTmpl.replaceById(Person.class).one(ppp.withFirstName("Dave"))));
TransactionResult txResult = result.block();
Person pFound = cbTmpl.findById(Person.class).one(person.id());
System.err.println("pFound " + pFound);
assertEquals(2, tryCount.get(), "should have been two tries. tries: " + tryCount.get());
assertEquals("Dave", pFound.getFirstname(), "should have been changed");
}
@DisplayName("Forcing CAS mismatch causes a transaction retry")
@Test
public void casMismatchCausesRetry() {
Person person = operations.insertById(Person.class).one(WalterWhite);
AtomicInteger attempts = new AtomicInteger();
couchbaseClientFactory.getCluster().reactive().transactions()
.run(ctx -> rxCBTmpl.findById(Person.class).one(person.id()).flatMap(fetched -> {
ReplaceLoopThread.updateOutOfTransaction(cbTmpl, fetched, attempts.incrementAndGet());
return rxCBTmpl.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction"));
})).block();
Person fetched = operations.findById(Person.class).one(person.id());
assertEquals("Changed by transaction", fetched.getFirstname());
assertEquals(2, attempts.get());
}
@Test
public void replacePersonCBTransactionsRxTmplRollback() {
String newName = "Walt";
Person person = cbTmpl.insertById(Person.class).one(WalterWhite);
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { //
return rxCBTmpl.findById(Person.class).one(person.id()) //
.flatMap(pp -> rxCBTmpl.replaceById(Person.class).one(pp.withFirstName(newName))).then(Mono.empty());
});
result.block();
Person pFound = cbTmpl.findById(Person.class).one(person.id());
System.err.println(pFound);
assertEquals(newName, pFound.getFirstname());
}
@Test
public void deletePersonCBTransactionsRxTmpl() {
Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite);
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get
// the
// ctx
return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person).then();
});
result.block();
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id());
assertNull(pFound, "Should not have found " + pFound);
}
@Test // ok
public void deletePersonCBTransactionsRxTmplFail() {
Person person = cbTmpl.insertById(Person.class).inCollection(cName).one(WalterWhite);
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get
// the
// ctx
return rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person)
.then(rxCBTmpl.removeById(Person.class).inCollection(cName).oneEntity(person));
});
assertThrowsWithCause(result::block, TransactionFailedException.class, DataRetrievalFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id());
assertEquals(pFound, person, "Should have found " + person);
}
@Test
public void deletePersonCBTransactionsRxRepo() {
Person person = repo.withCollection(cName).save(WalterWhite);
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get
// the
// ctx
return rxRepo.withCollection(cName).delete(person).then();
});
result.block();
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id());
assertNull(pFound, "Should not have found " + pFound);
}
@Test
public void deletePersonCBTransactionsRxRepoFail() {
Person person = repo.withCollection(cName).save(WalterWhite);
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> { // get
// the
// ctx
return rxRepo.withCollection(cName).findById(person.id())
.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.id());
assertEquals(pFound, person, "Should have found " + person + " instead of " + pFound);
}
@Test
public void findPersonCBTransactions() {
Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(WalterWhite);
List<Object> docs = new LinkedList<>();
Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId()));
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> {
return rxCBTmpl.findByQuery(Person.class).withConsistency(REQUEST_PLUS).inScope(sName).inCollection(cName)
.matching(q).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 = WalterWhite;
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions()
.run(ctx -> rxCBTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(person)
.<Person> flatMap(it -> Mono.error(new SimulateFailureException())));
assertThrowsWithCause(() -> result.block(), TransactionFailedException.class, SimulateFailureException.class);
Person pFound = cbTmpl.findById(Person.class).inCollection(cName).one(person.id());
assertNull(pFound, "Should not have found " + pFound);
}
@Test
public void replacePersonRbCBTransactions() {
Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(WalterWhite);
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> //
rxCBTmpl.findById(Person.class).inScope(sName).inCollection(cName).one(person.id()) //
.flatMap(pFound -> rxCBTmpl.replaceById(Person.class).inScope(sName).inCollection(cName)
.one(pFound.withFirstName("Walt")))
.<Person> 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.id());
assertEquals(person, pFound, "Should have found " + person + " instead of " + pFound);
}
@Test
public void findPersonSpringTransactions() {
Person person = cbTmpl.insertById(Person.class).inScope(sName).inCollection(cName).one(WalterWhite);
List<Object> docs = new LinkedList<>();
Query q = Query.query(QueryCriteria.where("meta().id").eq(person.getId()));
Mono<TransactionResult> result = couchbaseClientFactory.getCluster().reactive().transactions().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);
}
}
}

View File

@@ -0,0 +1,357 @@
/*
* Copyright 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.sdk;
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.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.junit.jupiter.api.AfterEach;
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.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.TransactionalSupport;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.domain.PersonWithoutVersion;
import org.springframework.data.couchbase.transactions.ReplaceLoopThread;
import org.springframework.data.couchbase.transactions.SimulateFailureException;
import org.springframework.data.couchbase.transactions.TransactionsConfig;
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.java.transactions.ReactiveTransactionAttemptContext;
import com.couchbase.client.java.transactions.TransactionResult;
import com.couchbase.client.java.transactions.config.TransactionOptions;
import com.couchbase.client.java.transactions.error.TransactionFailedException;
/**
* Tests using template methods (findById etc.) inside a regular reactive SDK transaction.
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
public class SDKReactiveTransactionsTemplateIntegrationTests extends JavaIntegrationTests {
// intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated.
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired ReactiveCouchbaseTemplate ops;
@Autowired CouchbaseTemplate blocking;
Person WalterWhite;
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
static class RunResult {
public final TransactionResult result;
public final int attempts;
public RunResult(TransactionResult result, int attempts) {
this.result = result;
this.attempts = attempts;
}
}
private RunResult doInTransaction(Function<ReactiveTransactionAttemptContext, Mono<?>> lambda) {
return doInTransaction(lambda, null);
}
private RunResult doInTransaction(Function<ReactiveTransactionAttemptContext, Mono<?>> lambda,
@Nullable TransactionOptions options) {
AtomicInteger attempts = new AtomicInteger();
TransactionResult result = couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> {
return TransactionalSupport.checkForTransactionInThreadLocalStorage().then(Mono.defer(() -> {
attempts.incrementAndGet();
return lambda.apply(ctx);
}));
}, options).block();
assertNotInTransaction();
return new RunResult(result, attempts.get());
}
@DisplayName("A basic golden path insert should succeed")
@Test
public void committedInsert() {
RunResult rr = doInTransaction(ctx -> {
return Mono.defer(() -> {
return ops.insertById(Person.class).one(WalterWhite);
});
});
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertEquals("Walter", fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path replace should succeed")
@Test
public void committedReplace() {
Person initial = blocking.insertById(Person.class).one(WalterWhite);
RunResult rr = doInTransaction(ctx -> {
return ops.findById(Person.class).one(WalterWhite.id()).flatMap(person -> {
person.setFirstname("changed");
return ops.replaceById(Person.class).one(person);
});
});
Person fetched = blocking.findById(Person.class).one(initial.id());
assertEquals("changed", fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path remove should succeed")
@Test
public void committedRemove() {
Person person = blocking.insertById(Person.class).one(WalterWhite);
RunResult rr = doInTransaction(ctx -> {
return ops.findById(Person.class).one(person.id())
.flatMap(fetched -> ops.removeById(Person.class).oneEntity(fetched));
});
Person fetched = blocking.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path removeByQuery should succeed")
@Test
public void committedRemoveByQuery() {
Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doInTransaction(ctx -> {
return ops.removeByQuery(Person.class).withConsistency(REQUEST_PLUS)
.matching(QueryCriteria.where("firstname").eq(WalterWhite.id())).all().then();
});
Person fetched = blocking.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path findByQuery should succeed")
@Test
public void committedFindByQuery() {
Person person = blocking.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doInTransaction(ctx -> {
return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(WalterWhite.getFirstname()))
.all().then();
});
assertEquals(1, rr.attempts);
}
@DisplayName("Basic test of doing an insert then rolling back")
@Test
public void rollbackInsert() {
AtomicInteger attempts = new AtomicInteger();
assertThrowsWithCause(() -> doInTransaction(ctx -> {
attempts.incrementAndGet();
return ops.insertById(Person.class).one(WalterWhite).map((p) -> throwSimulateFailureException(p));
}), TransactionFailedException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(WalterWhite.id());
assertNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a replace then rolling back")
@Test
public void rollbackReplace() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(ctx -> {
attempts.incrementAndGet();
return ops.findById(Person.class).one(person.id()) //
.flatMap(p -> ops.replaceById(Person.class).one(p.withFirstName("changed"))) //
.map(p -> throwSimulateFailureException(p));
}), TransactionFailedException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(person.id());
assertEquals("Walter", fetched.getFirstname());
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a remove then rolling back")
@Test
public void rollbackRemove() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(ctx -> {
attempts.incrementAndGet();
return ops.findById(Person.class).one(person.id()).flatMap(p -> ops.removeById(Person.class).oneEntity(p)) //
.doOnSuccess(p -> throwSimulateFailureException(p)); // remove has no result
}), TransactionFailedException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a removeByQuery then rolling back")
@Test
public void rollbackRemoveByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(ctx -> {
attempts.incrementAndGet();
return ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all()
.elementAt(0).map(p -> throwSimulateFailureException(p));
}), TransactionFailedException.class, SimulateFailureException.class);
Person fetched = blocking.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a findByQuery then rolling back")
@Test
public void rollbackFindByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(ctx -> {
attempts.incrementAndGet();
return ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all()
.elementAt(0).map(p -> throwSimulateFailureException(p));
}), TransactionFailedException.class, SimulateFailureException.class);
assertEquals(1, attempts.get());
}
@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 = blocking.insertById(Person.class).one(WalterWhite);
Person refetched = blocking.findById(Person.class).one(person.id());
refetched = blocking.replaceById(Person.class).one(refetched); // new cas
assertNotEquals(person.getVersion(), refetched.getVersion());
assertThrowsWithCause(() -> doInTransaction(ctx -> ops.replaceById(Person.class).one(person), // old cas
TransactionOptions.transactionOptions().timeout(Duration.ofSeconds(2))), TransactionFailedException.class,
Exception.class);
}
@DisplayName("Entity must have CAS field during replace")
@Test
public void replaceEntityWithoutCas() {
;
PersonWithoutVersion person = blocking.insertById(PersonWithoutVersion.class)
.one(new PersonWithoutVersion("Walter", "White"));
assertThrowsWithCause(
() -> doInTransaction(ctx -> ops.findById(PersonWithoutVersion.class).one(person.id())
.flatMap(fetched -> ops.replaceById(PersonWithoutVersion.class).one(fetched))),
TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have non-zero CAS during replace")
@Test
public void replaceEntityWithCasZero() {
Person person = blocking.insertById(Person.class).one(WalterWhite);
// switchedPerson here will have CAS=0, which will fail
Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds");
assertThrowsWithCause(() -> doInTransaction(ctx -> {
return ops.replaceById(Person.class).one(switchedPerson);
}), TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have CAS field during remove")
@Test
public void removeEntityWithoutCas() {
PersonWithoutVersion person = blocking.insertById(PersonWithoutVersion.class)
.one(new PersonWithoutVersion("Walter", "White"));
assertThrowsWithCause(() -> doInTransaction(ctx -> {
return ops.findById(PersonWithoutVersion.class).one(person.id())
.flatMap(fetched -> ops.removeById(PersonWithoutVersion.class).oneEntity(fetched));
}), TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS")
@Test
public void removeEntityById() {
Person person = blocking.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> doInTransaction(ctx -> {
return ops.findById(Person.class).one(person.id()).flatMap(p -> ops.removeById(Person.class).one(p.id()));
}), TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("Forcing CAS mismatch causes a transaction retry")
@Test
public void casMismatchCausesRetry() {
Person person = blocking.insertById(Person.class).one(WalterWhite);
AtomicInteger attempts = new AtomicInteger();
doInTransaction(ctx -> {
return ops.findById(Person.class).one(person.id()).flatMap(fetched -> Mono.defer(() -> {
ReplaceLoopThread.updateOutOfTransaction(blocking, person.withFirstName("Changed externally"),
attempts.incrementAndGet());
return ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction"));
}));
});
Person fetched = blocking.findById(Person.class).one(person.id());
assertEquals("Changed by transaction", fetched.getFirstname());
assertEquals(2, attempts.get());
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 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.sdk;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
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.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.transactions.TransactionsConfig;
import org.springframework.data.couchbase.transactions.util.TransactionTestUtil;
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 com.couchbase.client.java.transactions.error.TransactionFailedException;
/**
* Tests for regular SDK transactions, where Spring operations that aren't supported in a transaction are being used.
* They should be prevented at runtime.
*
* @Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(
classes = { TransactionsConfig.class, SDKTransactionsNonAllowableOperationsIntegrationTests.PersonService.class })
public class SDKTransactionsNonAllowableOperationsIntegrationTests extends JavaIntegrationTests {
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired PersonService personService;
Person WalterWhite;
@BeforeAll
public static void beforeAll() {
callSuperBeforeAll(new Object() {});
}
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
TransactionTestUtil.assertNotInTransaction();
}
void test(Consumer<CouchbaseOperations> r) {
AtomicInteger tryCount = new AtomicInteger(0);
assertThrowsWithCause(() -> {
couchbaseClientFactory.getCluster().transactions().run(ignored -> {
personService.doInService(tryCount, (ops) -> {
r.accept(ops);
return null;
});
});
}, TransactionFailedException.class, IllegalArgumentException.class);
assertEquals(1, tryCount.get());
}
@DisplayName("Using existsById() in a transaction is rejected at runtime")
@Test
public void existsById() {
test((ops) -> {
ops.existsById(Person.class).one(WalterWhite.id());
});
}
@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) -> {
ops.findFromReplicasById(Person.class).any(WalterWhite.id());
});
}
@DisplayName("Using upsertById() in a transaction is rejected at runtime")
@Test
public void upsertById() {
test((ops) -> {
ops.upsertById(Person.class).one(WalterWhite);
});
}
// This is intentionally not a @Transactional service
@Service
@Component
static class PersonService {
final CouchbaseOperations personOperations;
public PersonService(CouchbaseOperations ops) {
personOperations = ops;
}
public <T> T doInService(AtomicInteger tryCount, Function<CouchbaseOperations, T> callback) {
tryCount.incrementAndGet();
return callback.apply(personOperations);
}
}
}

View File

@@ -0,0 +1,421 @@
/*
* Copyright 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.sdk;
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.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertInTransaction;
import static org.springframework.data.couchbase.transactions.util.TransactionTestUtil.assertNotInTransaction;
import reactor.util.annotation.Nullable;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
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.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.RemoveResult;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.domain.PersonWithoutVersion;
import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException;
import org.springframework.data.couchbase.transactions.ReplaceLoopThread;
import org.springframework.data.couchbase.transactions.SimulateFailureException;
import org.springframework.data.couchbase.transactions.TransactionsConfig;
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.transaction.PreviousOperationFailedException;
import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.transactions.TransactionAttemptContext;
import com.couchbase.client.java.transactions.TransactionGetResult;
import com.couchbase.client.java.transactions.TransactionResult;
import com.couchbase.client.java.transactions.config.TransactionOptions;
import com.couchbase.client.java.transactions.error.TransactionFailedException;
/**
* Tests for using template methods (findById etc.) inside a regular SDK transaction.
*
* @author Graham Pople
*/
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(TransactionsConfig.class)
public class SDKTransactionsTemplateIntegrationTests extends JavaIntegrationTests {
// intellij flags "Could not autowire" when config classes are specified with classes={...}. But they are populated.
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired CouchbaseTemplate ops;
Person WalterWhite;
@BeforeEach
public void beforeEachTest() {
WalterWhite = new Person("Walter", "White");
assertNotInTransaction();
}
@AfterEach
public void afterEachTest() {
assertNotInTransaction();
}
static class RunResult {
public final TransactionResult result;
public final int attempts;
public RunResult(TransactionResult result, int attempts) {
this.result = result;
this.attempts = attempts;
}
}
private RunResult doInTransaction(Consumer<TransactionAttemptContext> lambda) {
return doInTransaction(lambda, null);
}
private RunResult doInTransaction(Consumer<TransactionAttemptContext> lambda, @Nullable TransactionOptions options) {
AtomicInteger attempts = new AtomicInteger();
TransactionResult result = couchbaseClientFactory.getCluster().transactions().run(ctx -> {
assertInTransaction();
attempts.incrementAndGet();
lambda.accept(ctx);
}, options);
assertNotInTransaction();
return new RunResult(result, attempts.get());
}
@DisplayName("A basic golden path insert should succeed")
@Test
public void committedInsert() {
RunResult rr = doInTransaction(ctx -> {
ops.insertById(Person.class).one(WalterWhite);
});
Person fetched = ops.findById(Person.class).one(WalterWhite.id());
assertEquals(WalterWhite.getFirstname(), fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path replace should succeed")
@Test
public void committedReplace() {
Person p = ops.insertById(Person.class).one(WalterWhite);
RunResult rr = doInTransaction(ctx -> {
Person person = ops.findById(Person.class).one(p.id());
person.setFirstname("changed");
ops.replaceById(Person.class).one(person);
});
Person fetched = ops.findById(Person.class).one(p.id());
assertEquals("changed", fetched.getFirstname());
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path remove should succeed")
@Test
public void committedRemove() {
Person person = ops.insertById(Person.class).one(WalterWhite);
RunResult rr = doInTransaction(ctx -> {
Person fetched = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).oneEntity(fetched);
});
Person fetched = ops.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path removeByQuery should succeed")
@Test
public void committedRemoveByQuery() {
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doInTransaction(ctx -> {
List<RemoveResult> removed = ops.removeByQuery(Person.class)
.matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
assertEquals(1, removed.size());
});
Person fetched = ops.findById(Person.class).one(person.id());
assertNull(fetched);
assertEquals(1, rr.attempts);
}
@DisplayName("A basic golden path findByQuery should succeed")
@Test
public void committedFindByQuery() {
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
RunResult rr = doInTransaction(ctx -> {
List<Person> found = ops.findByQuery(Person.class)
.matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
assertEquals(1, found.size());
});
assertEquals(1, rr.attempts);
}
@DisplayName("Basic test of doing an insert then rolling back")
@Test
public void rollbackInsert() {
AtomicInteger attempts = new AtomicInteger();
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
attempts.incrementAndGet();
Person person = ops.insertById(Person.class).one(WalterWhite);
throw new SimulateFailureException();
});
}, TransactionFailedException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(WalterWhite.id());
assertNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a replace then rolling back")
@Test
public void rollbackReplace() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
attempts.incrementAndGet();
Person p = ops.findById(Person.class).one(person.id());
p.setFirstname("changed");
ops.replaceById(Person.class).one(p);
throw new SimulateFailureException();
});
}, TransactionFailedException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(person.id());
assertEquals(WalterWhite.getFirstname(), fetched.getFirstname());
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a remove then rolling back")
@Test
public void rollbackRemove() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
attempts.incrementAndGet();
Person p = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).oneEntity(p);
throw new SimulateFailureException();
});
}, TransactionFailedException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(person.getId().toString());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a removeByQuery then rolling back")
@Test
public void rollbackRemoveByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
attempts.incrementAndGet();
ops.removeByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
throw new SimulateFailureException();
});
}, TransactionFailedException.class, SimulateFailureException.class);
Person fetched = ops.findById(Person.class).one(person.id());
assertNotNull(fetched);
assertEquals(1, attempts.get());
}
@DisplayName("Basic test of doing a findByQuery then rolling back")
@Test
public void rollbackFindByQuery() {
AtomicInteger attempts = new AtomicInteger();
Person person = ops.insertById(Person.class).one(WalterWhite.withIdFirstname());
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
attempts.incrementAndGet();
ops.findByQuery(Person.class).matching(QueryCriteria.where("firstname").eq(person.getFirstname())).all();
throw new SimulateFailureException();
});
}, TransactionFailedException.class, SimulateFailureException.class);
assertEquals(1, attempts.get());
}
@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 = ops.insertById(Person.class).one(WalterWhite);
Person refetched = ops.findById(Person.class).one(person.id());
ops.replaceById(Person.class).one(refetched);
assertNotEquals(person.getVersion(), refetched.getVersion());
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
ops.replaceById(Person.class).one(person);
}, TransactionOptions.transactionOptions().timeout(Duration.ofSeconds(2)));
}, TransactionFailedException.class);
}
@DisplayName("Entity must have CAS field during replace")
@Test
public void replaceEntityWithoutCas() {
PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class)
.one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"));
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id());
ops.replaceById(PersonWithoutVersion.class).one(fetched);
});
}, TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have non-zero CAS during replace")
@Test
public void replaceEntityWithCasZero() {
Person person = ops.insertById(Person.class).one(WalterWhite);
// switchedPerson here will have CAS=0, which will fail
Person switchedPerson = new Person(person.getId(), "Dave", "Reynolds");
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
ops.replaceById(Person.class).one(switchedPerson);
});
}, TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("Entity must have CAS field during remove")
@Test
public void removeEntityWithoutCas() {
PersonWithoutVersion person = ops.insertById(PersonWithoutVersion.class)
.one(new PersonWithoutVersion(UUID.randomUUID(), "Walter", "White"));
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
PersonWithoutVersion fetched = ops.findById(PersonWithoutVersion.class).one(person.id());
ops.removeById(PersonWithoutVersion.class).oneEntity(fetched);
});
}, TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("removeById().one(id) isn't allowed in transactions, since we don't have the CAS")
@Test
public void removeEntityById() {
Person person = ops.insertById(Person.class).one(WalterWhite);
assertThrowsWithCause(() -> {
doInTransaction(ctx -> {
Person p = ops.findById(Person.class).one(person.id());
ops.removeById(Person.class).one(p.getId().toString());
});
}, TransactionFailedException.class, IllegalArgumentException.class);
}
@DisplayName("Forcing CAS mismatch causes a transaction retry")
@Test
public void casMismatchCausesRetry() {
Person person = ops.insertById(Person.class).one(WalterWhite);
AtomicInteger attempts = new AtomicInteger();
doInTransaction(ctx -> {
Person fetched = ops.findById(Person.class).one(person.getId().toString());
ReplaceLoopThread.updateOutOfTransaction(ops, person.withFirstName("Changed externally"),
attempts.incrementAndGet());
try {
ops.replaceById(Person.class).one(fetched.withFirstName("Changed by transaction"));
} catch (RuntimeException err) {
assertTrue(err instanceof UncategorizedTransactionDataAccessException);
}
if (attempts.get() == 1) {
// Subsequent operations in this attempt should fast-fail
try {
ops.findById(Person.class).one(person.getId().toString());
} catch (RuntimeException err) {
assertTrue(err instanceof UncategorizedTransactionDataAccessException);
assertTrue(err.getCause() instanceof PreviousOperationFailedException);
}
}
});
Person fetched = ops.findById(Person.class).one(person.getId().toString());
assertEquals("Changed by transaction", fetched.getFirstname());
assertEquals(2, attempts.get());
}
@DisplayName("Using the standard ctx.get(), ctx.replace() API works as expected")
@Test
public void casMismatchUsingRegularTransactionOperations() {
Person person = ops.insertById(Person.class).one(WalterWhite);
AtomicInteger attempts = new AtomicInteger();
doInTransaction(ctx -> {
TransactionGetResult gr = ctx.get(couchbaseClientFactory.getDefaultCollection(), WalterWhite.id());
ReplaceLoopThread.updateOutOfTransaction(ops, person.withFirstName("Changed externally"),
attempts.incrementAndGet());
try {
ctx.replace(gr, JsonObject.create());
} catch (RuntimeException err) {
assertTrue(err instanceof TransactionOperationFailedException);
}
if (attempts.get() == 1) {
// Subsequent operations in this attempt should fast-fail
try {
ctx.get(couchbaseClientFactory.getDefaultCollection(), WalterWhite.id());
} catch (RuntimeException err) {
assertTrue(err instanceof TransactionOperationFailedException);
assertTrue(err.getCause() instanceof PreviousOperationFailedException);
}
}
});
assertEquals(2, attempts.get());
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.transactions.util;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.springframework.data.couchbase.core.TransactionalSupport;
/**
* Utility methods for transaction tests.
*
* @author Graham Pople
*/
public class TransactionTestUtil {
private TransactionTestUtil() {}
public static void assertInTransaction() {
assertTrue(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent());
}
public static void assertNotInTransaction() {
assertFalse(TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent());
}
}

View File

@@ -46,10 +46,14 @@ import com.couchbase.client.core.error.IndexFailureException;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions;
import com.couchbase.client.java.manager.query.CreateQueryIndexOptions;
import com.couchbase.client.java.transactions.config.TransactionsCleanupConfig;
import com.couchbase.client.java.transactions.config.TransactionsConfig;
/**
* Parent class which drives all dynamic integration tests based on the configured cluster setup.
*
* @author Michael Reiche
*
* @since 2.0.0
*/
@ExtendWith(ClusterInvocationProvider.class)
@@ -61,8 +65,13 @@ public abstract class ClusterAwareIntegrationTests {
@BeforeAll
static void setup(TestClusterConfig config) {
testClusterConfig = config;
try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(),
authenticator(), bucketName(), null, environment().build())) {
// 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

View File

@@ -27,12 +27,6 @@ import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMP
import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE;
import static org.springframework.data.couchbase.util.Util.waitUntilCondition;
import okhttp3.Credentials;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
@@ -47,11 +41,17 @@ import java.util.function.Predicate;
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;
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.transactions.SimulateFailureException;
import com.couchbase.client.core.diagnostics.PingResult;
import com.couchbase.client.core.diagnostics.PingState;
@@ -88,7 +88,7 @@ import com.couchbase.client.java.search.result.SearchResult;
/**
* Extends the {@link ClusterAwareIntegrationTests} with java-client specific code.
*
* @Author Michael Reiche
* @author Michael Reiche
*/
// Temporarily increased timeout to (possibly) workaround MB-37011 when Developer Preview enabled
@Timeout(value = 10, unit = TimeUnit.MINUTES) // Safety timer so tests can't block CI executors
@@ -100,10 +100,18 @@ public class JavaIntegrationTests extends ClusterAwareIntegrationTests {
@BeforeAll
public static void beforeAll() {
Config.setScopeName(null);
callSuperBeforeAll(new Object() {});
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE);
reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE);
try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(),
authenticator(), bucketName())) {
couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(),
CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true));
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
/**
@@ -203,9 +211,8 @@ public class JavaIntegrationTests extends ClusterAwareIntegrationTests {
}
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) {}
}
}
@@ -215,33 +222,6 @@ public class JavaIntegrationTests extends ClusterAwareIntegrationTests {
}
}
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().split("=")[0].replace("11210", "8091").replace("11207", "18091");
String protocol = hostPort.equals("18091") ? "https" : "http";
String bucketname = UUID.randomUUID().toString();
try {
Response postResponse = httpClient.newCall(new Request.Builder()
.header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword()))
.url(protocol + "://" + hostPort + "/pools/default/buckets/")
.post(new FormBody.Builder().add("name", bucketname).add("bucketType", "membase").add("ramQuotaMB", "100")
.add("replicaNumber", Integer.toString(0)).add("flushEnabled", "1").build())
.build()).execute();
if (postResponse.code() != 202) {
throw new IOException("Could not create bucket: " + postResponse + ", Reason: " + postResponse.body().string());
}
Response deleteResponse = httpClient.newCall(new Request.Builder()
.header("Authorization", Credentials.basic(config().adminUsername(), config().adminPassword()))
.url(protocol + "://" + hostPort + "/pools/default/buckets/" + bucketname).delete().build()).execute();
System.out.println("deleteResponse: " + deleteResponse);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* Improve test stability by waiting for a given service to report itself ready.
*/
@@ -401,4 +381,61 @@ public class JavaIntegrationTests extends ClusterAwareIntegrationTests {
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 = "Expected one of " + toString(expectedTypes) + " but was : " + actualException.getClass();
throw new AssertionFailedError(message, actualException);
}
String message = "Expected one of " + toString(expectedTypes) + " to be thrown, but nothing was thrown.";
throw new AssertionFailedError(message);
}
private static String toString(Object[] array) {
StringBuffer sb = new StringBuffer();
sb.append("[");
for (int i = 0; i < array.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(array[i]);
}
sb.append("]");
return sb.toString();
}
public static void assertThrowsWithCause(Executable executable, Class<?>... expectedTypes) {
try {
executable.execute();
} catch (Throwable actualException) {
for (Class<?> expectedType : expectedTypes) {
if (actualException == null || !expectedType.isAssignableFrom(actualException.getClass())) {
String message = "Expected " + expectedType + " to be thrown/cause, but found " + actualException;
throw new AssertionFailedError(message, actualException);
}
actualException = actualException.getCause();
}
UnrecoverableExceptions.rethrowIfUnrecoverable(actualException);
return;
}
String message = "Expected " + expectedTypes[0] + " to be thrown, but nothing was thrown.";
throw new AssertionFailedError(message);
}
// Use this to still rely on the return type
public static <T> T throwSimulateFailureException(T entity) {
throw new SimulateFailureException();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors
* 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.
@@ -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 @@ abstract class TestCluster implements ExtensionContext.Store.CloseableResource {
defaults.load(url.openStream());
}
} catch (Exception ex) {
throw new RuntimeException("Could not load properties", ex);
throw new RuntimeException("Could not load properties - maybe <packaging> is pom instead of jar?", ex);
}
Properties all = new Properties(System.getProperties());

View File

@@ -25,6 +25,8 @@ import java.util.Set;
* tests for bootstrapping their own code.
*
* @since 2.0.0
*
* @author Michael Reiche
*/
public class TestClusterConfig {
@@ -83,6 +85,7 @@ public class TestClusterConfig {
* Finds the first node with a given service enabled in the config.
* <p>
* This method can be used to find bootstrap nodes and similar.
* </p>
*
* @param service the service to find.
* @return a node config if found, empty otherwise.

View File

@@ -146,4 +146,27 @@ public class Util {
return Pair.of(unexpected, missing);
}
}
/**
* 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);
}
}

View File

@@ -22,10 +22,12 @@
- log additional debug info during automatic index creation
-->
<logger name="org.springframework.data.couchbase.core" level="trace"/>"
<logger name="org.springframework.data.couchbase.core" level="debug"/>"
<logger name="org.springframework.data.couchbase.repository.query" level="debug"/>
<logger name="org.springframework.data.couchbase.repository.query.SpatialViewQueryCreator" level="trace"/>
<logger name="org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery" level="trace"/>
<logger name="org.springframework.data.couchbase.repository.support.IndexManager" level="debug"/>
<logger name="com.couchbase.transactions" level="debug"/>"
<logger name="org.springframework.data.couchbase.transaction" level="trace"/>
</configuration>