From 6365b3c05f08e421ba18e70b2a8ed97e0511ad5a Mon Sep 17 00:00:00 2001 From: Toshihiro Suzuki Date: Fri, 20 Jun 2025 06:27:10 +0000 Subject: [PATCH 1/2] Empty commit [skip ci] From 137b6724f68aa36f677a89b815981d02c3eab877 Mon Sep 17 00:00:00 2001 From: Toshihiro Suzuki Date: Fri, 20 Jun 2025 15:26:59 +0900 Subject: [PATCH 2/2] Add READ_COMMITTED isolation (#2803) --- .../consensuscommit/ConsensusCommit.java | 2 + .../ConsensusCommitManager.java | 50 +- .../consensuscommit/CrudHandler.java | 52 +- .../consensuscommit/Isolation.java | 1 + .../consensuscommit/RecoveryExecutor.java | 74 +- .../transaction/consensuscommit/Snapshot.java | 17 +- .../TwoPhaseConsensusCommit.java | 2 + .../ConsensusCommitConfigTest.java | 2 +- .../ConsensusCommitManagerTest.java | 12 +- .../consensuscommit/ConsensusCommitTest.java | 12 + .../consensuscommit/CrudHandlerTest.java | 570 +- .../consensuscommit/RecoveryExecutorTest.java | 387 +- .../TwoPhaseConsensusCommitTest.java | 13 + ...CommitNullMetadataIntegrationTestBase.java | 3 +- ...nsusCommitSpecificIntegrationTestBase.java | 6559 ++++++++++------- 15 files changed, 4821 insertions(+), 2935 deletions(-) diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java index 08571505fb..20fcbf89c8 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java @@ -198,6 +198,8 @@ public void commit() throws CommitException, UnknownTransactionStatusException { try { crud.waitForRecoveryCompletionIfNecessary(); + } catch (CrudConflictException e) { + throw new CommitConflictException(e.getMessage(), e, getId()); } catch (CrudException e) { throw new CommitException(e.getMessage(), e, getId()); } diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java index 442975841f..fe08f2322b 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java @@ -48,12 +48,12 @@ public class ConsensusCommitManager extends AbstractDistributedTransactionManage private static final Logger logger = LoggerFactory.getLogger(ConsensusCommitManager.class); private final DistributedStorage storage; private final DistributedStorageAdmin admin; - private final ConsensusCommitConfig config; private final TransactionTableMetadataManager tableMetadataManager; private final Coordinator coordinator; private final ParallelExecutor parallelExecutor; private final RecoveryExecutor recoveryExecutor; protected final CommitHandler commit; + private final Isolation isolation; private final boolean isIncludeMetadataEnabled; private final ConsensusCommitMutationOperationChecker mutationOperationChecker; @Nullable private final CoordinatorGroupCommitter groupCommitter; @@ -65,7 +65,7 @@ public ConsensusCommitManager( super(databaseConfig); this.storage = storage; this.admin = admin; - config = new ConsensusCommitConfig(databaseConfig); + ConsensusCommitConfig config = new ConsensusCommitConfig(databaseConfig); coordinator = new Coordinator(storage, config); parallelExecutor = new ParallelExecutor(config); tableMetadataManager = @@ -74,7 +74,8 @@ public ConsensusCommitManager( RecoveryHandler recovery = new RecoveryHandler(storage, coordinator, tableMetadataManager); recoveryExecutor = new RecoveryExecutor(coordinator, recovery, tableMetadataManager); groupCommitter = CoordinatorGroupCommitter.from(config).orElse(null); - commit = createCommitHandler(); + commit = createCommitHandler(config); + isolation = config.getIsolation(); isIncludeMetadataEnabled = config.isIncludeMetadataEnabled(); mutationOperationChecker = new ConsensusCommitMutationOperationChecker(tableMetadataManager); } @@ -85,7 +86,7 @@ protected ConsensusCommitManager(DatabaseConfig databaseConfig) { storage = storageFactory.getStorage(); admin = storageFactory.getStorageAdmin(); - config = new ConsensusCommitConfig(databaseConfig); + ConsensusCommitConfig config = new ConsensusCommitConfig(databaseConfig); coordinator = new Coordinator(storage, config); parallelExecutor = new ParallelExecutor(config); tableMetadataManager = @@ -94,7 +95,8 @@ protected ConsensusCommitManager(DatabaseConfig databaseConfig) { RecoveryHandler recovery = new RecoveryHandler(storage, coordinator, tableMetadataManager); recoveryExecutor = new RecoveryExecutor(coordinator, recovery, tableMetadataManager); groupCommitter = CoordinatorGroupCommitter.from(config).orElse(null); - commit = createCommitHandler(); + commit = createCommitHandler(config); + isolation = config.getIsolation(); isIncludeMetadataEnabled = config.isIncludeMetadataEnabled(); mutationOperationChecker = new ConsensusCommitMutationOperationChecker(tableMetadataManager); } @@ -104,17 +106,17 @@ protected ConsensusCommitManager(DatabaseConfig databaseConfig) { ConsensusCommitManager( DistributedStorage storage, DistributedStorageAdmin admin, - ConsensusCommitConfig config, DatabaseConfig databaseConfig, Coordinator coordinator, ParallelExecutor parallelExecutor, RecoveryExecutor recoveryExecutor, CommitHandler commit, + Isolation isolation, + boolean isIncludeMetadataEnabled, @Nullable CoordinatorGroupCommitter groupCommitter) { super(databaseConfig); this.storage = storage; this.admin = admin; - this.config = config; tableMetadataManager = new TransactionTableMetadataManager( admin, databaseConfig.getMetadataCacheExpirationTimeSecs()); @@ -123,13 +125,14 @@ protected ConsensusCommitManager(DatabaseConfig databaseConfig) { this.recoveryExecutor = recoveryExecutor; this.commit = commit; this.groupCommitter = groupCommitter; - this.isIncludeMetadataEnabled = config.isIncludeMetadataEnabled(); + this.isolation = isolation; + this.isIncludeMetadataEnabled = isIncludeMetadataEnabled; this.mutationOperationChecker = new ConsensusCommitMutationOperationChecker(tableMetadataManager); } // `groupCommitter` must be set before calling this method. - private CommitHandler createCommitHandler() { + private CommitHandler createCommitHandler(ConsensusCommitConfig config) { if (isGroupCommitEnabled()) { return new CommitHandlerWithGroupCommit( storage, @@ -156,7 +159,7 @@ public DistributedTransaction begin() { @Override public DistributedTransaction begin(String txId) { - return begin(txId, config.getIsolation(), false, false); + return begin(txId, isolation, false, false); } @Override @@ -167,14 +170,15 @@ public DistributedTransaction beginReadOnly() { @Override public DistributedTransaction beginReadOnly(String txId) { - return begin(txId, config.getIsolation(), true, false); + return begin(txId, isolation, true, false); } /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @Deprecated @Override public DistributedTransaction start(com.scalar.db.api.Isolation isolation) { - return begin(Isolation.valueOf(isolation.name())); + String txId = UUID.randomUUID().toString(); + return begin(txId, Isolation.valueOf(isolation.name()), false, false); } /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @@ -189,14 +193,16 @@ public DistributedTransaction start(String txId, com.scalar.db.api.Isolation iso @Override public DistributedTransaction start( com.scalar.db.api.Isolation isolation, com.scalar.db.api.SerializableStrategy strategy) { - return begin(Isolation.valueOf(isolation.name())); + String txId = UUID.randomUUID().toString(); + return begin(txId, Isolation.valueOf(isolation.name()), false, false); } /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @Deprecated @Override public DistributedTransaction start(com.scalar.db.api.SerializableStrategy strategy) { - return begin(Isolation.SERIALIZABLE); + String txId = UUID.randomUUID().toString(); + return begin(txId, Isolation.SERIALIZABLE, false, false); } /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @@ -217,18 +223,6 @@ public DistributedTransaction start( return begin(txId, Isolation.valueOf(isolation.name()), false, false); } - @VisibleForTesting - DistributedTransaction begin(Isolation isolation) { - String txId = UUID.randomUUID().toString(); - return begin(txId, isolation, false, false); - } - - @VisibleForTesting - DistributedTransaction beginReadOnly(Isolation isolation) { - String txId = UUID.randomUUID().toString(); - return begin(txId, isolation, true, false); - } - @VisibleForTesting DistributedTransaction begin( String txId, Isolation isolation, boolean readOnly, boolean oneOperation) { @@ -238,7 +232,7 @@ DistributedTransaction begin( assert groupCommitter != null; txId = groupCommitter.reserve(txId); } - if (!config.getIsolation().equals(isolation)) { + if (!this.isolation.equals(isolation)) { logger.warn( "Setting different isolation level from the one in DatabaseConfig might cause unexpected " + "anomalies"); @@ -266,7 +260,7 @@ DistributedTransaction begin( private DistributedTransaction beginOneOperation(boolean readOnly) { String txId = UUID.randomUUID().toString(); - return begin(txId, config.getIsolation(), readOnly, true); + return begin(txId, isolation, readOnly, true); } @Override diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/CrudHandler.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/CrudHandler.java index 2aa11d6a71..0dadeaeca4 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/CrudHandler.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/CrudHandler.java @@ -25,6 +25,7 @@ import com.scalar.db.common.AbstractTransactionCrudOperableScanner; import com.scalar.db.common.error.CoreError; import com.scalar.db.exception.storage.ExecutionException; +import com.scalar.db.exception.transaction.CrudConflictException; import com.scalar.db.exception.transaction.CrudException; import com.scalar.db.util.ScalarDbUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -196,8 +197,26 @@ Optional read(@Nullable Snapshot.Key key, Get get) throws Cru private Optional executeRecovery( Snapshot.Key key, Selection selection, TransactionResult result) throws CrudException { + RecoveryExecutor.RecoveryType recoveryType; + if (snapshot.getIsolation() == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + if (readOnly) { + // In read-only mode, we don't recover the record, but return the committed result + recoveryType = RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_NOT_RECOVER; + } else { + // In read-write mode, we recover the record and return the committed result + recoveryType = RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER; + } + } else { + // In SNAPSHOT or SERIALIZABLE isolation, we always recover the record and return the latest + // result + recoveryType = RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER; + } + RecoveryExecutor.Result recoveryResult = - recoveryExecutor.execute(key, selection, result, snapshot.getId()); + recoveryExecutor.execute(key, selection, result, snapshot.getId(), recoveryType); + recoveryResults.add(recoveryResult); return recoveryResult.recoveredResult; } @@ -337,8 +356,8 @@ private void putIntoReadSetInSnapshot(Snapshot.Key key, Optional result) { - // If neither validation nor snapshot read is required, we don't need to put the result into + // If neither validation nor snapshot reads are required, we don't need to put the result into // the get set if (isValidationOrSnapshotReadRequired()) { snapshot.putIntoGetSet(get, result); @@ -355,7 +374,7 @@ private void putIntoGetSetInSnapshot(Get get, Optional result private void putIntoScanSetInSnapshot( Scan scan, LinkedHashMap results) { - // If neither validation nor snapshot read is required, we don't need to put the results into + // If neither validation nor snapshot reads are required, we don't need to put the results into // the scan set if (isValidationOrSnapshotReadRequired()) { snapshot.putIntoScanSet(scan, results); @@ -371,12 +390,16 @@ private void putIntoScannerSetInSnapshot( } private void verifyNoOverlap(Scan scan, Map results) { - // In either read-only mode or one-operation mode, we don't need to verify the overlap - if (!readOnly && !oneOperation) { + if (isOverlapVerificationRequired()) { snapshot.verifyNoOverlap(scan, results); } } + private boolean isOverlapVerificationRequired() { + // In either read-only mode or one-operation mode, we don't need to verify overlap + return !readOnly && !oneOperation; + } + public void put(Put put) throws CrudException { Snapshot.Key key = new Snapshot.Key(put); @@ -476,6 +499,7 @@ private Get createGet(Snapshot.Key key) { * complete, the validation could fail due to records with PREPARED or DELETED status. * * + * @throws CrudConflictException if any recovery task fails due to a conflict * @throws CrudException if any recovery task fails */ public void waitForRecoveryCompletionIfNecessary() throws CrudException { @@ -487,6 +511,11 @@ public void waitForRecoveryCompletionIfNecessary() throws CrudException { recoveryResult.recoveryFuture.get(); } } catch (java.util.concurrent.ExecutionException e) { + if (e.getCause() instanceof CrudConflictException) { + throw new CrudConflictException( + e.getCause().getMessage(), e.getCause(), snapshot.getId()); + } + throw new CrudException( CoreError.CONSENSUS_COMMIT_RECOVERING_RECORDS_FAILED.buildMessage( e.getCause().getMessage()), @@ -507,6 +536,11 @@ void waitForRecoveryCompletion() throws CrudException { try { recoveryResult.recoveryFuture.get(); } catch (java.util.concurrent.ExecutionException e) { + if (e.getCause() instanceof CrudConflictException) { + throw new CrudConflictException( + e.getCause().getMessage(), e.getCause(), snapshot.getId()); + } + throw new CrudException( CoreError.CONSENSUS_COMMIT_RECOVERING_RECORDS_FAILED.buildMessage( e.getCause().getMessage()), @@ -737,10 +771,10 @@ public ConsensusCommitStorageScanner(Scan scan, List originalProjections scanner = scanFromStorage(scan); } - if (isValidationOrSnapshotReadRequired()) { + if (isValidationOrSnapshotReadRequired() || isOverlapVerificationRequired()) { results = new LinkedHashMap<>(); } else { - // If neither validation nor snapshot read is required, we don't need to put the results + // If neither validation nor snapshot reads are required, we don't need to put the results // into the scan set results = null; } diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/Isolation.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/Isolation.java index 4942bf4fe3..1ca70db103 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/Isolation.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/Isolation.java @@ -1,6 +1,7 @@ package com.scalar.db.transaction.consensuscommit; public enum Isolation { + READ_COMMITTED, SNAPSHOT, SERIALIZABLE, } diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutor.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutor.java index 20372718b3..c240b44d41 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutor.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutor.java @@ -3,6 +3,7 @@ import static com.scalar.db.transaction.consensuscommit.ConsensusCommitUtils.createAfterImageColumnsFromBeforeImage; import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.Uninterruptibles; import com.scalar.db.api.Operation; @@ -62,22 +63,65 @@ public RecoveryExecutor( } public Result execute( - Snapshot.Key key, Selection selection, TransactionResult result, String transactionId) + Snapshot.Key key, + Selection selection, + TransactionResult result, + String transactionId, + RecoveryType recoveryType) throws CrudException { assert !result.isCommitted(); - Optional state = getCoordinatorState(result.getId()); + Optional recoveredResult; + Future future; - Optional recoveredResult = - createRecoveredResult(state, selection, result, transactionId); + switch (recoveryType) { + case RETURN_LATEST_RESULT_AND_RECOVER: + Optional state = getCoordinatorState(result.getId()); - // Recover the record - Future future = - executorService.submit( - () -> { - recovery.recover(selection, result, state); - return null; - }); + throwUncommittedRecordExceptionIfTransactionNotExpired( + state, selection, result, transactionId); + + // Return the latest result + recoveredResult = createRecoveredResult(state, selection, result, transactionId); + + // Recover the record + future = + executorService.submit( + () -> { + recovery.recover(selection, result, state); + return null; + }); + + break; + case RETURN_COMMITTED_RESULT_AND_RECOVER: + // Return the committed result + recoveredResult = createRecordFromBeforeImage(selection, result, transactionId); + + // Recover the record + future = + executorService.submit( + () -> { + Optional s = getCoordinatorState(result.getId()); + + throwUncommittedRecordExceptionIfTransactionNotExpired( + s, selection, result, transactionId); + + recovery.recover(selection, result, s); + return null; + }); + + break; + case RETURN_COMMITTED_RESULT_AND_NOT_RECOVER: + // Return the committed result + recoveredResult = createRecordFromBeforeImage(selection, result, transactionId); + + // No need to recover the record + future = Futures.immediateFuture(null); + + break; + default: + throw new AssertionError("Unknown recovery type: " + recoveryType); + } return new Result(key, recoveredResult, future); } @@ -97,8 +141,6 @@ private Optional createRecoveredResult( TransactionResult result, String transactionId) throws CrudException { - throwUncommittedRecordExceptionIfTransactionNotExpired(state, selection, result, transactionId); - if (!state.isPresent() || state.get().getState() == TransactionState.ABORTED) { return createRecordFromBeforeImage(selection, result, transactionId); } else { @@ -272,4 +314,10 @@ public Result( this.recoveryFuture = recoveryFuture; } } + + public enum RecoveryType { + RETURN_LATEST_RESULT_AND_RECOVER, + RETURN_COMMITTED_RESULT_AND_RECOVER, + RETURN_COMMITTED_RESULT_AND_NOT_RECOVER + } } diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java index cfd6dc4982..f01b5397d3 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java @@ -47,6 +47,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.NotThreadSafe; import org.slf4j.Logger; @@ -65,11 +66,11 @@ public class Snapshot { private final ConcurrentMap> readSet; // The get set stores information about the records retrieved by Get operations in this - // transaction. This is used for validation and snapshot read. + // transaction. This is used for validation and snapshot reads. private final ConcurrentMap> getSet; // The scan set stores information about the records retrieved by Scan operations in this - // transaction. This is used for validation and snapshot read. + // transaction. This is used for validation and snapshot reads. private final Map> scanSet; // The scanner set stores information about scanners that are not fully scanned. This is used for @@ -128,9 +129,8 @@ public String getId() { return id; } - @VisibleForTesting @Nonnull - Isolation getIsolation() { + public Isolation getIsolation() { return isolation; } @@ -214,6 +214,11 @@ public boolean containsKeyInReadSet(Key key) { return readSet.containsKey(key); } + @Nullable + public Optional getFromReadSet(Key key) { + return readSet.get(key); + } + public boolean containsKeyInGetSet(Get get) { return getSet.containsKey(get); } @@ -760,6 +765,10 @@ private boolean isSerializable() { return isolation == Isolation.SERIALIZABLE; } + public boolean isSnapshotReadRequired() { + return isolation != Isolation.READ_COMMITTED; + } + public boolean isValidationRequired() { return isSerializable(); } diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java index 36b32e1791..ee36e31488 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommit.java @@ -195,6 +195,8 @@ public void prepare() throws PreparationException { try { crud.waitForRecoveryCompletionIfNecessary(); + } catch (CrudConflictException e) { + throw new PreparationConflictException(e.getMessage(), e, getId()); } catch (CrudException e) { throw new PreparationException(e.getMessage(), e, getId()); } diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitConfigTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitConfigTest.java index d6ad3811c5..263724c5dd 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitConfigTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitConfigTest.java @@ -62,7 +62,7 @@ public void constructor_PropertiesWithDeprecatedIsolationLevelGiven_ShouldLoadPr public void constructor_UnsupportedIsolationGiven_ShouldThrowIllegalArgumentException() { // Arrange Properties props = new Properties(); - props.setProperty(ConsensusCommitConfig.ISOLATION_LEVEL, "READ_COMMITTED"); + props.setProperty(ConsensusCommitConfig.ISOLATION_LEVEL, "READ_UNCOMMITTED"); // Act Assert assertThatThrownBy(() -> new ConsensusCommitConfig(new DatabaseConfig(props))) diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java index 77152177fa..6d26ef4fde 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java @@ -60,7 +60,6 @@ public class ConsensusCommitManagerTest { @Mock private DistributedStorage storage; @Mock private DistributedStorageAdmin admin; @Mock private DatabaseConfig databaseConfig; - @Mock private ConsensusCommitConfig consensusCommitConfig; @Mock private Coordinator coordinator; @Mock private ParallelExecutor parallelExecutor; @Mock private RecoveryExecutor recoveryExecutor; @@ -76,15 +75,14 @@ public void setUp() throws Exception { new ConsensusCommitManager( storage, admin, - consensusCommitConfig, databaseConfig, coordinator, parallelExecutor, recoveryExecutor, commit, + Isolation.SNAPSHOT, + false, null); - - when(consensusCommitConfig.getIsolation()).thenReturn(Isolation.SNAPSHOT); } @Test @@ -127,12 +125,13 @@ public void begin_TxIdGiven_ReturnWithSpecifiedTxIdAndSnapshotIsolation() { new ConsensusCommitManager( storage, admin, - consensusCommitConfig, databaseConfig, coordinator, parallelExecutor, recoveryExecutor, commit, + Isolation.SNAPSHOT, + false, groupCommitter); // Act @@ -158,12 +157,13 @@ public void begin_TxIdGiven_ReturnWithSpecifiedTxIdAndSnapshotIsolation() { new ConsensusCommitManager( storage, admin, - consensusCommitConfig, databaseConfig, coordinator, parallelExecutor, recoveryExecutor, commit, + Isolation.SNAPSHOT, + false, groupCommitter); // Act diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java index 95b837a748..9398fe29a4 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java @@ -571,6 +571,18 @@ public void commit_ScannerNotClosed_ShouldThrowIllegalStateException() { assertThatThrownBy(() -> consensus.commit()).isInstanceOf(IllegalStateException.class); } + @Test + public void + commit_CrudConflictExceptionThrownByCrudHandlerWaitForRecoveryCompletionIfNecessary_ShouldThrowCommitConflictException() + throws CrudException { + // Arrange + when(crud.getSnapshot()).thenReturn(snapshot); + doThrow(CrudConflictException.class).when(crud).waitForRecoveryCompletionIfNecessary(); + + // Act Assert + assertThatThrownBy(() -> consensus.commit()).isInstanceOf(CommitConflictException.class); + } + @Test public void commit_CrudExceptionThrownByCrudHandlerWaitForRecoveryCompletionIfNecessary_ShouldThrowCommitException() diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/CrudHandlerTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/CrudHandlerTest.java index cd2816ab2c..aeabe2e77c 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/CrudHandlerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/CrudHandlerTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -119,6 +120,11 @@ public void setUp() throws Exception { .thenReturn(new TransactionTableMetadata(TABLE_METADATA)); when(tableMetadataManager.getTransactionTableMetadata(any(), any())) .thenReturn(new TransactionTableMetadata(TABLE_METADATA)); + + // Default behavior for snapshot isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(true); + when(snapshot.isValidationRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.SNAPSHOT); } private Get prepareGet() { @@ -400,8 +406,90 @@ public void get_GetExistsInSnapshot_ShouldReturnFromSnapshot() throws CrudExcept } @Test - public void get_GetNotExistsInSnapshotAndRecordInStorageNotCommitted_ShouldCallRecoveryExecutor() - throws ExecutionException, CrudException { + public void + get_GetNotExistsInSnapshotAndRecordInStorageCommitted_ReadCommittedIsolation_ShouldReturnFromStorageAndUpdateSnapshot() + throws CrudException, ExecutionException { + // Arrange + + // For READ_COMMITTED isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.READ_COMMITTED); + + Get get = Get.newBuilder(prepareGet()).build(); + Get getForStorage = toGetForStorageFrom(get); + + Optional expected = Optional.of(prepareResult(TransactionState.COMMITTED)); + Optional transactionResult = expected.map(e -> (TransactionResult) e); + Snapshot.Key key = new Snapshot.Key(getForStorage); + when(snapshot.containsKeyInGetSet(getForStorage)).thenReturn(false); + when(storage.get(any())).thenReturn(expected); + when(snapshot.mergeResult(key, transactionResult, getForStorage.getConjunctions())) + .thenReturn(transactionResult); + + // Act + Optional result = handler.get(get); + + // Assert + assertThat(result) + .isEqualTo( + Optional.of( + new FilteredResult( + expected.get(), Collections.emptyList(), TABLE_METADATA, false))); + verify(storage).get(getForStorage); + verify(snapshot).putIntoReadSet(key, Optional.of((TransactionResult) expected.get())); + verify(snapshot, never()).putIntoGetSet(any(), any()); + } + + @Test + public void + get_GetNotExistsInSnapshotAndRecordInStorageCommitted_ReadCommittedIsolation_InReadOnlyMode_ShouldReturnFromStorageAndNotUpdateSnapshot() + throws CrudException, ExecutionException { + // Arrange + handler = + new CrudHandler( + storage, + snapshot, + recoveryExecutor, + tableMetadataManager, + false, + mutationConditionsValidator, + parallelExecutor, + true, + false); + + // For READ_COMMITTED isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.READ_COMMITTED); + + Get get = Get.newBuilder(prepareGet()).build(); + Get getForStorage = toGetForStorageFrom(get); + + Optional expected = Optional.of(prepareResult(TransactionState.COMMITTED)); + Optional transactionResult = expected.map(e -> (TransactionResult) e); + Snapshot.Key key = new Snapshot.Key(getForStorage); + when(snapshot.containsKeyInGetSet(getForStorage)).thenReturn(false); + when(storage.get(any())).thenReturn(expected); + when(snapshot.mergeResult(key, transactionResult, getForStorage.getConjunctions())) + .thenReturn(transactionResult); + + // Act + Optional result = handler.get(get); + + // Assert + assertThat(result) + .isEqualTo( + Optional.of( + new FilteredResult( + expected.get(), Collections.emptyList(), TABLE_METADATA, false))); + verify(storage).get(getForStorage); + verify(snapshot, never()).putIntoReadSet(any(), any()); + verify(snapshot, never()).putIntoGetSet(any(), any()); + } + + @Test + public void + get_GetNotExistsInSnapshotAndRecordInStorageNotCommitted_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover() + throws ExecutionException, CrudException { // Arrange Get get = prepareGet(); Snapshot.Key key = new Snapshot.Key(get); @@ -421,7 +509,12 @@ public void get_GetNotExistsInSnapshotAndRecordInStorageNotCommitted_ShouldCallR @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, getForStorage, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + getForStorage, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -429,7 +522,13 @@ public void get_GetNotExistsInSnapshotAndRecordInStorageNotCommitted_ShouldCallR // Assert verify(storage).get(getForStorage); - verify(recoveryExecutor).execute(key, getForStorage, new TransactionResult(result), ANY_ID_1); + verify(recoveryExecutor) + .execute( + key, + getForStorage, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); verify(snapshot).putIntoReadSet(key, Optional.of(recoveredResult)); verify(snapshot).putIntoGetSet(getForStorage, Optional.of(recoveredResult)); @@ -439,6 +538,137 @@ public void get_GetNotExistsInSnapshotAndRecordInStorageNotCommitted_ShouldCallR new FilteredResult(expected, Collections.emptyList(), TABLE_METADATA, false))); } + @Test + public void + get_GetNotExistsInSnapshotAndRecordInStorageNotCommitted_ReadCommittedIsolation_ShouldCallRecoveryExecutorWithReturnCommittedResultAndNotRecover() + throws ExecutionException, CrudException { + // Arrange + handler = + new CrudHandler( + storage, + snapshot, + recoveryExecutor, + tableMetadataManager, + false, + mutationConditionsValidator, + parallelExecutor, + true, + false); + + // For READ_COMMITTED isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.READ_COMMITTED); + + Get get = prepareGet(); + Snapshot.Key key = new Snapshot.Key(get); + Get getForStorage = toGetForStorageFrom(get); + result = prepareResult(TransactionState.PREPARED); + when(storage.get(getForStorage)).thenReturn(Optional.of(result)); + when(snapshot.containsKeyInGetSet(getForStorage)).thenReturn(false); + when(snapshot.getId()).thenReturn(ANY_ID_1); + + TransactionResult expected = mock(TransactionResult.class); + when(expected.getContainedColumnNames()).thenReturn(Collections.singleton(ANY_NAME_1)); + when(expected.getAsObject(ANY_NAME_1)).thenReturn(ANY_TEXT_1); + + TransactionResult transactionResult = new TransactionResult(result); + + TransactionResult recoveredResult = mock(TransactionResult.class); + @SuppressWarnings("unchecked") + Future recoveryFuture = mock(Future.class); + + when(recoveryExecutor.execute( + key, + getForStorage, + transactionResult, + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_NOT_RECOVER)) + .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); + + when(snapshot.mergeResult(key, Optional.of(recoveredResult), getForStorage.getConjunctions())) + .thenReturn(Optional.of(expected)); + + // Act + Optional actual = handler.get(get); + + // Assert + verify(storage).get(getForStorage); + verify(recoveryExecutor) + .execute( + key, + getForStorage, + transactionResult, + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_NOT_RECOVER); + verify(snapshot, never()).putIntoReadSet(any(), any()); + verify(snapshot, never()).putIntoGetSet(any(), any()); + + assertThat(actual) + .isEqualTo( + Optional.of( + new FilteredResult(expected, Collections.emptyList(), TABLE_METADATA, false))); + } + + @Test + public void + get_GetNotExistsInSnapshotAndRecordInStorageNotCommitted_ReadCommittedIsolation_InReadOnlyMode_ShouldCallRecoveryExecutorWithReturnCommittedResultAndRecover() + throws ExecutionException, CrudException { + // Arrange + + // For READ_COMMITTED isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.READ_COMMITTED); + + Get get = prepareGet(); + Snapshot.Key key = new Snapshot.Key(get); + Get getForStorage = toGetForStorageFrom(get); + result = prepareResult(TransactionState.PREPARED); + when(storage.get(getForStorage)).thenReturn(Optional.of(result)); + when(snapshot.containsKeyInGetSet(getForStorage)).thenReturn(false); + when(snapshot.getId()).thenReturn(ANY_ID_1); + + TransactionResult expected = mock(TransactionResult.class); + when(expected.getContainedColumnNames()).thenReturn(Collections.singleton(ANY_NAME_1)); + when(expected.getAsObject(ANY_NAME_1)).thenReturn(ANY_TEXT_1); + + TransactionResult transactionResult = new TransactionResult(result); + + TransactionResult recoveredResult = mock(TransactionResult.class); + @SuppressWarnings("unchecked") + Future recoveryFuture = mock(Future.class); + + when(recoveryExecutor.execute( + key, + getForStorage, + transactionResult, + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER)) + .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); + + when(snapshot.mergeResult(key, Optional.of(recoveredResult), getForStorage.getConjunctions())) + .thenReturn(Optional.of(expected)); + + // Act + Optional actual = handler.get(get); + + // Assert + verify(storage).get(getForStorage); + verify(recoveryExecutor) + .execute( + key, + getForStorage, + transactionResult, + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + verify(snapshot).putIntoReadSet(key, Optional.of(recoveredResult)); + verify(snapshot, never()).putIntoGetSet(any(), any()); + + assertThat(actual) + .isEqualTo( + Optional.of( + new FilteredResult(expected, Collections.emptyList(), TABLE_METADATA, false))); + } + @Test public void get_GetNotExistsInSnapshotAndRecordNotExistsInStorage_ShouldReturnEmpty() throws CrudException, ExecutionException { @@ -500,6 +730,43 @@ public void get_CalledTwice_SecondTimeShouldReturnTheSameFromSnapshot() verify(storage).get(getForStorage); } + @Test + public void get_CalledTwice_ReadCommittedIsolation_BothShouldReturnFromStorage() + throws ExecutionException, CrudException { + // Arrange + + // For READ_COMMITTED isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.READ_COMMITTED); + + Get originalGet = prepareGet(); + Get getForStorage = toGetForStorageFrom(originalGet); + Get get1 = prepareGet(); + Get get2 = prepareGet(); + Result result = prepareResult(TransactionState.COMMITTED); + Optional expected = Optional.of(new TransactionResult(result)); + Snapshot.Key key = new Snapshot.Key(getForStorage); + when(snapshot.containsKeyInGetSet(getForStorage)).thenReturn(false); + when(snapshot.mergeResult( + key, Optional.of(new TransactionResult(result)), getForStorage.getConjunctions())) + .thenReturn(expected); + when(storage.get(getForStorage)).thenReturn(Optional.of(result)); + + // Act + Optional results1 = handler.get(get1); + Optional results2 = handler.get(get2); + + // Assert + verify(storage, times(2)).get(getForStorage); + verify(snapshot, times(2)).putIntoReadSet(key, expected); + assertThat(results1) + .isEqualTo( + Optional.of( + new FilteredResult( + expected.get(), Collections.emptyList(), TABLE_METADATA, false))); + assertThat(results1).isEqualTo(results2); + } + @Test public void get_CalledTwiceUnderRealSnapshot_SecondTimeShouldReturnTheSameFromSnapshot() throws ExecutionException, CrudException { @@ -538,6 +805,44 @@ public void get_CalledTwiceUnderRealSnapshot_SecondTimeShouldReturnTheSameFromSn verify(storage).get(getForStorage); } + @Test + public void get_CalledTwiceUnderRealSnapshot_ReadCommittedIsolation_BothShouldReturnFromStorage() + throws ExecutionException, CrudException { + // Arrange + Get originalGet = prepareGet(); + Get getForStorage = toGetForStorageFrom(originalGet); + Get get1 = prepareGet(); + Get get2 = prepareGet(); + Result result = prepareResult(TransactionState.COMMITTED); + Optional expected = Optional.of(new TransactionResult(result)); + snapshot = + new Snapshot(ANY_TX_ID, Isolation.READ_COMMITTED, tableMetadataManager, parallelExecutor); + handler = + new CrudHandler( + storage, + snapshot, + recoveryExecutor, + tableMetadataManager, + false, + parallelExecutor, + false, + false); + when(storage.get(getForStorage)).thenReturn(Optional.of(result)); + + // Act + Optional results1 = handler.get(get1); + Optional results2 = handler.get(get2); + + // Assert + verify(storage, times(2)).get(getForStorage); + assertThat(results1) + .isEqualTo( + Optional.of( + new FilteredResult( + expected.get(), Collections.emptyList(), TABLE_METADATA, false))); + assertThat(results1).isEqualTo(results2); + } + @Test public void get_ForNonExistingTable_ShouldThrowIllegalArgumentException() throws ExecutionException { @@ -754,8 +1059,9 @@ void scanOrGetScanner_ResultGivenFromStorage_InReadOnlyMode_ShouldUpdateSnapshot @ParameterizedTest @EnumSource(ScanType.class) - void scanOrGetScanner_PreparedResultGivenFromStorage_ShouldCallRecoveryExecutor(ScanType scanType) - throws ExecutionException, IOException, CrudException { + void + scanOrGetScanner_PreparedResultGivenFromStorage_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover( + ScanType scanType) throws ExecutionException, IOException, CrudException { // Arrange Scan scan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(scan); @@ -779,7 +1085,12 @@ void scanOrGetScanner_PreparedResultGivenFromStorage_ShouldCallRecoveryExecutor( @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, scanForStorage, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + scanForStorage, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -798,6 +1109,127 @@ void scanOrGetScanner_PreparedResultGivenFromStorage_ShouldCallRecoveryExecutor( new FilteredResult(recoveredResult, Collections.emptyList(), TABLE_METADATA, false)); } + @ParameterizedTest + @EnumSource(ScanType.class) + void + scanOrGetScanner_PreparedResultGivenFromStorage_ReadCommittedIsolation_ShouldCallRecoveryExecutorWithReturnCommittedResultAndRecover( + ScanType scanType) throws ExecutionException, IOException, CrudException { + // Arrange + + // For READ_COMMITTED isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.READ_COMMITTED); + + Scan scan = prepareScan(); + Scan scanForStorage = toScanForStorageFrom(scan); + + result = prepareResult(TransactionState.PREPARED); + if (scanType == ScanType.SCAN) { + when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + } else { + when(scanner.one()).thenReturn(Optional.of(result)).thenReturn(Optional.empty()); + } + when(storage.scan(scanForStorage)).thenReturn(scanner); + + Snapshot.Key key = new Snapshot.Key(scan, result); + + when(snapshot.getId()).thenReturn(ANY_ID_1); + + TransactionResult recoveredResult = mock(TransactionResult.class); + when(recoveredResult.getContainedColumnNames()).thenReturn(Collections.singleton(ANY_NAME_1)); + when(recoveredResult.getAsObject(ANY_NAME_1)).thenReturn(ANY_TEXT_1); + + @SuppressWarnings("unchecked") + Future recoveryFuture = mock(Future.class); + + when(recoveryExecutor.execute( + key, + scanForStorage, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER)) + .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); + + // Act + List results = scanOrGetScanner(scan, scanType); + + // Assert + verify(scanner).close(); + verify(snapshot).putIntoReadSet(key, Optional.of(recoveredResult)); + verify(snapshot, never()).putIntoScanSet(any(), any()); + verify(snapshot).verifyNoOverlap(scanForStorage, ImmutableMap.of(key, recoveredResult)); + + assertThat(results) + .containsExactly( + new FilteredResult(recoveredResult, Collections.emptyList(), TABLE_METADATA, false)); + } + + @ParameterizedTest + @EnumSource(ScanType.class) + void + scanOrGetScanner_PreparedResultGivenFromStorage_ReadCommittedIsolation_InReadOnlyMode_ShouldCallRecoveryExecutorWithReturnCommittedResultAndNotRecover( + ScanType scanType) throws ExecutionException, IOException, CrudException { + // Arrange + handler = + new CrudHandler( + storage, + snapshot, + recoveryExecutor, + tableMetadataManager, + false, + mutationConditionsValidator, + parallelExecutor, + true, + false); + + // For READ_COMMITTED isolation + when(snapshot.isSnapshotReadRequired()).thenReturn(false); + when(snapshot.getIsolation()).thenReturn(Isolation.READ_COMMITTED); + + Scan scan = prepareScan(); + Scan scanForStorage = toScanForStorageFrom(scan); + + result = prepareResult(TransactionState.PREPARED); + if (scanType == ScanType.SCAN) { + when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + } else { + when(scanner.one()).thenReturn(Optional.of(result)).thenReturn(Optional.empty()); + } + when(storage.scan(scanForStorage)).thenReturn(scanner); + + Snapshot.Key key = new Snapshot.Key(scan, result); + + when(snapshot.getId()).thenReturn(ANY_ID_1); + + TransactionResult recoveredResult = mock(TransactionResult.class); + when(recoveredResult.getContainedColumnNames()).thenReturn(Collections.singleton(ANY_NAME_1)); + when(recoveredResult.getAsObject(ANY_NAME_1)).thenReturn(ANY_TEXT_1); + + @SuppressWarnings("unchecked") + Future recoveryFuture = mock(Future.class); + + when(recoveryExecutor.execute( + key, + scanForStorage, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_NOT_RECOVER)) + .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); + + // Act + List results = scanOrGetScanner(scan, scanType); + + // Assert + verify(scanner).close(); + verify(snapshot, never()).putIntoReadSet(any(), any()); + verify(snapshot, never()).putIntoScanSet(any(), any()); + verify(snapshot, never()).verifyNoOverlap(any(), any()); + + assertThat(results) + .containsExactly( + new FilteredResult(recoveredResult, Collections.emptyList(), TABLE_METADATA, false)); + } + @ParameterizedTest @EnumSource(ScanType.class) void scanOrGetScanner_CalledTwice_SecondTimeShouldReturnTheSameFromSnapshot(ScanType scanType) @@ -1072,7 +1504,7 @@ void scanOrGetScanner_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgum @ParameterizedTest @EnumSource(ScanType.class) void - scanOrGetScanner_CrossPartitionScanAndPreparedResultFromStorageGiven_RecoveredRecordMatchesConjunction_ShouldCallRecoveryExecutor( + scanOrGetScanner_CrossPartitionScanAndPreparedResultFromStorageGiven_RecoveredRecordMatchesConjunction_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover( ScanType scanType) throws ExecutionException, IOException, CrudException { // Arrange Scan scan = prepareCrossPartitionScan(); @@ -1099,7 +1531,12 @@ void scanOrGetScanner_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgum @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, scanForStorage, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + scanForStorage, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -1128,7 +1565,7 @@ void scanOrGetScanner_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgum @ParameterizedTest @EnumSource(ScanType.class) void - scanOrGetScanner_CrossPartitionScanAndPreparedResultFromStorageGiven_RecoveredRecordDoesNotMatchConjunction_ShouldCallRecoveryExecutor( + scanOrGetScanner_CrossPartitionScanAndPreparedResultFromStorageGiven_RecoveredRecordDoesNotMatchConjunction_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover( ScanType scanType) throws ExecutionException, IOException, CrudException { // Arrange Scan scan = prepareCrossPartitionScan(); @@ -1155,7 +1592,12 @@ void scanOrGetScanner_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgum @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, scanForStorage, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + scanForStorage, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -1265,8 +1707,9 @@ void scanOrGetScanner_WithLimitExceedingAvailableResults_ShouldReturnAllAvailabl @ParameterizedTest @EnumSource(ScanType.class) - void scanOrGetScanner_WithLimit_UncommittedResult_ShouldCallRecoveryExecutor(ScanType scanType) - throws ExecutionException, IOException, CrudException { + void + scanOrGetScanner_WithLimit_UncommittedResult_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover( + ScanType scanType) throws ExecutionException, IOException, CrudException { // Arrange Scan scanWithoutLimit = prepareScan(); Scan scanWithLimit = Scan.newBuilder(scanWithoutLimit).limit(2).build(); @@ -1313,14 +1756,26 @@ void scanOrGetScanner_WithLimit_UncommittedResult_ShouldCallRecoveryExecutor(Sca Future recoveryFuture = mock(Future.class); when(recoveryExecutor.execute( - key1, scanForStorageWithLimit, new TransactionResult(uncommittedResult1), ANY_ID_1)) + key1, + scanForStorageWithLimit, + new TransactionResult(uncommittedResult1), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key1, Optional.empty(), recoveryFuture)); when(recoveryExecutor.execute( - key2, scanForStorageWithLimit, new TransactionResult(uncommittedResult2), ANY_ID_1)) + key2, + scanForStorageWithLimit, + new TransactionResult(uncommittedResult2), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn( new RecoveryExecutor.Result(key2, Optional.of(recoveredResult1), recoveryFuture)); when(recoveryExecutor.execute( - key3, scanForStorageWithLimit, new TransactionResult(uncommittedResult3), ANY_ID_1)) + key3, + scanForStorageWithLimit, + new TransactionResult(uncommittedResult3), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn( new RecoveryExecutor.Result(key3, Optional.of(recoveredResult2), recoveryFuture)); @@ -1900,7 +2355,7 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() @Test public void - readUnread_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_ShouldCallRecoveryExecutor() + readUnread_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover() throws ExecutionException, CrudException { // Arrange Snapshot.Key key = mock(Snapshot.Key.class); @@ -1924,7 +2379,12 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, getForKey, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + getForKey, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -1932,14 +2392,20 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() // Assert verify(storage).get(getForKey); - verify(recoveryExecutor).execute(key, getForKey, new TransactionResult(result), ANY_ID_1); + verify(recoveryExecutor) + .execute( + key, + getForKey, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); verify(snapshot).putIntoReadSet(key, Optional.of(recoveredResult)); verify(snapshot).putIntoGetSet(getForKey, Optional.of(recoveredResult)); } @Test public void - readUnread_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_RecoveredRecordIsEmpty_ShouldCallRecoveryExecutor() + readUnread_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_RecoveredRecordIsEmpty_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover() throws ExecutionException, CrudException { // Arrange Snapshot.Key key = mock(Snapshot.Key.class); @@ -1963,7 +2429,12 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, getForKey, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + getForKey, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, recoveredRecord, recoveryFuture)); // Act @@ -1971,14 +2442,20 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() // Assert verify(storage).get(getForKey); - verify(recoveryExecutor).execute(key, getForKey, new TransactionResult(result), ANY_ID_1); + verify(recoveryExecutor) + .execute( + key, + getForKey, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); verify(snapshot).putIntoReadSet(key, recoveredRecord); verify(snapshot).putIntoGetSet(getForKey, recoveredRecord); } @Test public void - readUnread_GetWithConjunctionGiven_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_RecoveredRecordMatchesConjunction_ShouldCallRecoveryExecutor() + readUnread_GetWithConjunctionGiven_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_RecoveredRecordMatchesConjunction_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover() throws ExecutionException, CrudException { // Arrange Snapshot.Key key = mock(Snapshot.Key.class); @@ -2008,7 +2485,12 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, getWithConjunction, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + getWithConjunction, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -2023,14 +2505,19 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() .or(column(Attribute.BEFORE_PREFIX + ANY_NAME_3).isEqualToText(ANY_TEXT_3)) .build()); verify(recoveryExecutor) - .execute(key, getWithConjunction, new TransactionResult(result), ANY_ID_1); + .execute( + key, + getWithConjunction, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); verify(snapshot).putIntoReadSet(key, Optional.of(recoveredResult)); verify(snapshot).putIntoGetSet(getWithConjunction, Optional.of(recoveredResult)); } @Test public void - readUnread_GetWithConjunctionGiven_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_RecoveredRecordDoesNotMatchConjunction_ShouldCallRecoveryExecutor() + readUnread_GetWithConjunctionGiven_GetNotContainedInGetSet_UncommittedRecordReturnedByStorage_RecoveredRecordDoesNotMatchConjunction_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover() throws ExecutionException, CrudException { // Arrange Snapshot.Key key = mock(Snapshot.Key.class); @@ -2060,7 +2547,12 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, getWithConjunction, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + getWithConjunction, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -2075,7 +2567,12 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() .or(column(Attribute.BEFORE_PREFIX + ANY_NAME_3).isEqualToText(ANY_TEXT_3)) .build()); verify(recoveryExecutor) - .execute(key, getWithConjunction, new TransactionResult(result), ANY_ID_1); + .execute( + key, + getWithConjunction, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); verify(snapshot, never()).putIntoReadSet(any(), any()); verify(snapshot).putIntoGetSet(getWithConjunction, Optional.empty()); } @@ -2134,7 +2631,7 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() @Test public void - readUnread_NullKeyAndGetWithIndexNotContainedInGetSet_UncommittedRecordReturnedByStorage_ShouldCallRecoveryExecutor() + readUnread_NullKeyAndGetWithIndexNotContainedInGetSet_UncommittedRecordReturnedByStorage_ShouldCallRecoveryExecutorWithReturnLatestResultAndRecover() throws ExecutionException, CrudException { // Arrange when(result.getInt(Attribute.STATE)).thenReturn(TransactionState.PREPARED.get()); @@ -2157,7 +2654,12 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() @SuppressWarnings("unchecked") Future recoveryFuture = mock(Future.class); - when(recoveryExecutor.execute(key, getWithIndex, new TransactionResult(result), ANY_ID_1)) + when(recoveryExecutor.execute( + key, + getWithIndex, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .thenReturn(new RecoveryExecutor.Result(key, Optional.of(recoveredResult), recoveryFuture)); // Act @@ -2165,7 +2667,13 @@ public void readUnread_GetContainedInGetSet_ShouldCallAppropriateMethods() // Assert verify(storage).get(getWithIndex); - verify(recoveryExecutor).execute(key, getWithIndex, new TransactionResult(result), ANY_ID_1); + verify(recoveryExecutor) + .execute( + key, + getWithIndex, + new TransactionResult(result), + ANY_ID_1, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); verify(snapshot).putIntoReadSet(key, Optional.of(recoveredResult)); verify(snapshot).putIntoGetSet(getWithIndex, Optional.of(recoveredResult)); } diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutorTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutorTest.java index db14fe7b3e..3b61c0d764 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutorTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/RecoveryExecutorTest.java @@ -320,15 +320,23 @@ private TransactionResult prepareRolledForwardResult() { } @Test - public void execute_CoordinatorExceptionByCoordinatorState_ShouldThrowCrudException() - throws CoordinatorException, ExecutionException { + public void + execute_ReturnLatestResultAndRecoverType_CoordinatorExceptionByCoordinatorState_ShouldThrowCrudException() + throws CoordinatorException, ExecutionException { // Arrange TransactionResult transactionResult = mock(TransactionResult.class); when(transactionResult.getId()).thenReturn(ANY_ID_1); when(coordinator.getState(ANY_ID_1)).thenThrow(new CoordinatorException("error")); // Act Assert - assertThatThrownBy(() -> executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3)) + assertThatThrownBy( + () -> + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .isInstanceOf(CrudException.class); // Verify no recovery attempted @@ -337,7 +345,7 @@ public void execute_CoordinatorExceptionByCoordinatorState_ShouldThrowCrudExcept @Test public void - execute_TransactionNotExpiredAndNoCoordinatorState_ShouldThrowUncommittedRecordException() + execute_ReturnLatestResultAndRecoverType_TransactionNotExpiredAndNoCoordinatorState_ShouldThrowUncommittedRecordException() throws CoordinatorException, ExecutionException { // Arrange TransactionResult transactionResult = mock(TransactionResult.class); @@ -346,7 +354,14 @@ public void execute_CoordinatorExceptionByCoordinatorState_ShouldThrowCrudExcept when(recovery.isTransactionExpired(transactionResult)).thenReturn(false); // Act Assert - assertThatThrownBy(() -> executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3)) + assertThatThrownBy( + () -> + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER)) .isInstanceOf(UncommittedRecordException.class); // Verify no recovery attempted @@ -354,7 +369,9 @@ public void execute_CoordinatorExceptionByCoordinatorState_ShouldThrowCrudExcept } @Test - public void execute_TransactionExpiredAndNoCoordinatorState_ShouldRollback() throws Exception { + public void + execute_ReturnLatestResultAndRecoverType_TransactionExpiredAndNoCoordinatorState_ShouldRollback() + throws Exception { // Arrange TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.empty()); @@ -362,7 +379,12 @@ public void execute_TransactionExpiredAndNoCoordinatorState_ShouldRollback() thr // Act RecoveryExecutor.Result result = - executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3); + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); // Wait for recovery to complete result.recoveryFuture.get(); @@ -374,7 +396,7 @@ public void execute_TransactionExpiredAndNoCoordinatorState_ShouldRollback() thr @Test public void - execute_TransactionExpiredAndNoCoordinatorState_RecordWithoutBeforeImage_ShouldRollback() + execute_ReturnLatestResultAndRecoverType_TransactionExpiredAndNoCoordinatorState_RecordWithoutBeforeImage_ShouldRollback() throws Exception { // Arrange TransactionResult transactionResult = @@ -384,7 +406,12 @@ public void execute_TransactionExpiredAndNoCoordinatorState_ShouldRollback() thr // Act RecoveryExecutor.Result result = - executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3); + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); // Wait for recovery to complete result.recoveryFuture.get(); @@ -395,7 +422,8 @@ public void execute_TransactionExpiredAndNoCoordinatorState_ShouldRollback() thr } @Test - public void execute_CoordinatorStateIsAborted_ShouldRollback() throws Exception { + public void execute_ReturnLatestResultAndRecoverType_CoordinatorStateIsAborted_ShouldRollback() + throws Exception { // Arrange TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); Coordinator.State abortedState = new Coordinator.State(ANY_ID_2, TransactionState.ABORTED); @@ -403,7 +431,12 @@ public void execute_CoordinatorStateIsAborted_ShouldRollback() throws Exception // Act RecoveryExecutor.Result result = - executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3); + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); // Wait for recovery to complete result.recoveryFuture.get(); @@ -414,8 +447,9 @@ public void execute_CoordinatorStateIsAborted_ShouldRollback() throws Exception } @Test - public void execute_CoordinatorStateIsAborted_RecordWithoutBeforeImage_ShouldRollback() - throws Exception { + public void + execute_ReturnLatestResultAndRecoverType_CoordinatorStateIsAborted_RecordWithoutBeforeImage_ShouldRollback() + throws Exception { // Arrange TransactionResult transactionResult = prepareResultWithoutBeforeImage(TransactionState.PREPARED); @@ -424,7 +458,12 @@ public void execute_CoordinatorStateIsAborted_RecordWithoutBeforeImage_ShouldRol // Act RecoveryExecutor.Result result = - executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3); + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); // Wait for recovery to complete result.recoveryFuture.get(); @@ -435,8 +474,9 @@ public void execute_CoordinatorStateIsAborted_RecordWithoutBeforeImage_ShouldRol } @Test - public void execute_CoordinatorStateIsCommitted_RecordWithPreparedState_ShouldCommit() - throws Exception { + public void + execute_ReturnLatestResultAndRecoverType_CoordinatorStateIsCommitted_RecordWithPreparedState_ShouldCommit() + throws Exception { // Arrange TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); Coordinator.State commitState = new Coordinator.State(ANY_ID_2, TransactionState.COMMITTED); @@ -447,7 +487,12 @@ public void execute_CoordinatorStateIsCommitted_RecordWithPreparedState_ShouldCo // Act RecoveryExecutor.Result result = - executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3); + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); // Wait for recovery to complete result.recoveryFuture.get(); @@ -458,24 +503,324 @@ public void execute_CoordinatorStateIsCommitted_RecordWithPreparedState_ShouldCo } @Test - public void execute_CoordinatorStateIsCommitted_RecordWithDeletedState_ShouldCommit() - throws Exception { + public void + execute_ReturnLatestResultAndRecoverType_CoordinatorStateIsCommitted_RecordWithDeletedState_ShouldCommit() + throws Exception { // Arrange TransactionResult transactionResult = prepareResult(TransactionState.DELETED); Coordinator.State commitState = new Coordinator.State(ANY_ID_2, TransactionState.COMMITTED); when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.of(commitState)); + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_LATEST_RESULT_AND_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).isNotPresent(); + verify(recovery).recover(eq(selection), eq(transactionResult), eq(Optional.of(commitState))); + } + + @Test + public void + execute_ReturnCommittedResultAndRecoverType_CoordinatorExceptionByCoordinatorState_ShouldThrowExecutionExceptionCausedByCrudExceptionByRecoveryFuture() + throws CoordinatorException, ExecutionException, CrudException { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); + when(coordinator.getState(ANY_ID_2)).thenThrow(new CoordinatorException("error")); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + + // Assert + assertThatThrownBy(result.recoveryFuture::get) + .isInstanceOf(java.util.concurrent.ExecutionException.class) + .hasCauseInstanceOf(CrudException.class); + + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); + + // Verify no recovery attempted + verify(recovery, never()).recover(any(), any(), any()); + } + + @Test + public void + execute_ReturnCommittedResultAndRecoverType_TransactionNotExpiredAndNoCoordinatorState_ShouldThrowExecutionExceptionCauseByUncommittedRecordException() + throws CoordinatorException, ExecutionException, CrudException { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); + when(coordinator.getState(ANY_ID_1)).thenReturn(Optional.empty()); + when(recovery.isTransactionExpired(transactionResult)).thenReturn(false); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + + // Assert + assertThatThrownBy(result.recoveryFuture::get) + .isInstanceOf(java.util.concurrent.ExecutionException.class) + .hasCauseInstanceOf(UncommittedRecordException.class); + + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); + + // Verify no recovery attempted + verify(recovery, never()).recover(any(), any(), any()); + } + + @Test + public void + execute_ReturnCommittedResultAndRecoverType_TransactionExpiredAndNoCoordinatorState_ShouldRollback() + throws Exception { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); + when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.empty()); + when(recovery.isTransactionExpired(transactionResult)).thenReturn(true); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); + verify(recovery).recover(eq(selection), eq(transactionResult), eq(Optional.empty())); + } + + @Test + public void + execute_ReturnCommittedResultAndRecoverType_TransactionExpiredAndNoCoordinatorState_RecordWithoutBeforeImage_ShouldRollback() + throws Exception { + // Arrange + TransactionResult transactionResult = + prepareResultWithoutBeforeImage(TransactionState.PREPARED); + when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.empty()); + when(recovery.isTransactionExpired(transactionResult)).thenReturn(true); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).isEmpty(); + verify(recovery).recover(eq(selection), eq(transactionResult), eq(Optional.empty())); + } + + @Test + public void execute_ReturnCommittedResultAndRecoverType_CoordinatorStateIsAborted_ShouldRollback() + throws Exception { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); + Coordinator.State abortedState = new Coordinator.State(ANY_ID_2, TransactionState.ABORTED); + when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.of(abortedState)); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); + verify(recovery).recover(eq(selection), eq(transactionResult), eq(Optional.of(abortedState))); + } + + @Test + public void + execute_ReturnCommittedResultAndRecoverType_CoordinatorStateIsAborted_RecordWithoutBeforeImage_ShouldRollback() + throws Exception { + // Arrange + TransactionResult transactionResult = + prepareResultWithoutBeforeImage(TransactionState.PREPARED); + Coordinator.State abortedState = new Coordinator.State(ANY_ID_2, TransactionState.ABORTED); + when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.of(abortedState)); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).isEmpty(); + verify(recovery).recover(eq(selection), eq(transactionResult), eq(Optional.of(abortedState))); + } + + @Test + public void + execute_ReturnCommittedResultAndRecoverType_CoordinatorStateIsCommitted_RecordWithPreparedState_ShouldCommit() + throws Exception { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); + Coordinator.State commitState = new Coordinator.State(ANY_ID_2, TransactionState.COMMITTED); + when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.of(commitState)); + executor = spy(executor); + doReturn(ANY_TIME_MILLIS_4).when(executor).getCommittedAt(); // Act RecoveryExecutor.Result result = - executor.execute(snapshotKey, selection, transactionResult, ANY_ID_3); + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); // Wait for recovery to complete result.recoveryFuture.get(); // Assert - assertThat(result.recoveredResult).isNotPresent(); + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); + verify(recovery).recover(eq(selection), eq(transactionResult), eq(Optional.of(commitState))); + } + + @Test + public void + execute_ReturnCommittedResultAndRecoverType_CoordinatorStateIsCommitted_RecordWithDeletedState_ShouldCommit() + throws Exception { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.DELETED); + Coordinator.State commitState = new Coordinator.State(ANY_ID_2, TransactionState.COMMITTED); + when(coordinator.getState(ANY_ID_2)).thenReturn(Optional.of(commitState)); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); verify(recovery).recover(eq(selection), eq(transactionResult), eq(Optional.of(commitState))); } + + @Test + public void + execute_ReturnCommittedResultAndNotRecoverType_RecordWithoutBeforeImage_ShouldNotRecover() + throws Exception { + // Arrange + TransactionResult transactionResult = + prepareResultWithoutBeforeImage(TransactionState.PREPARED); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_NOT_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).isEmpty(); + + // Verify no recovery attempted + verify(recovery, never()).recover(any(), any(), any()); + } + + @Test + public void + execute_ReturnCommittedResultAndNotRecoverType_RecordWithPreparedState_ShouldNotRecover() + throws Exception { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.PREPARED); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_NOT_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); + + // Verify no recovery attempted + verify(recovery, never()).recover(any(), any(), any()); + } + + @Test + public void + execute_ReturnCommittedResultAndNotRecoverType_RecordWithDeletedState_ShouldNotRecover() + throws Exception { + // Arrange + TransactionResult transactionResult = prepareResult(TransactionState.DELETED); + + // Act + RecoveryExecutor.Result result = + executor.execute( + snapshotKey, + selection, + transactionResult, + ANY_ID_3, + RecoveryExecutor.RecoveryType.RETURN_COMMITTED_RESULT_AND_NOT_RECOVER); + + // Wait for recovery to complete + result.recoveryFuture.get(); + + // Assert + assertThat(result.recoveredResult).hasValue(prepareRolledBackResult()); + + // Verify no recovery attempted + verify(recovery, never()).recover(any(), any(), any()); + } } diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java index 6ef7fc4979..0703acfeac 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitTest.java @@ -550,6 +550,19 @@ public void prepare_ScannerNotClosed_ShouldThrowIllegalStateException() { assertThatThrownBy(() -> transaction.prepare()).isInstanceOf(IllegalStateException.class); } + @Test + public void + prepare_CrudConflictExceptionThrownByCrudHandlerWaitForRecoveryCompletionIfNecessary_ShouldThrowPreparationConflictException() + throws CrudException { + // Arrange + when(crud.getSnapshot()).thenReturn(snapshot); + doThrow(CrudConflictException.class).when(crud).waitForRecoveryCompletionIfNecessary(); + + // Act Assert + assertThatThrownBy(() -> transaction.prepare()) + .isInstanceOf(PreparationConflictException.class); + } + @Test public void prepare_CrudExceptionThrownByCrudHandlerWaitForRecoveryCompletionIfNecessary_ShouldThrowPreparationException() diff --git a/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitNullMetadataIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitNullMetadataIntegrationTestBase.java index 7c8301ae9d..a62c047733 100644 --- a/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitNullMetadataIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitNullMetadataIntegrationTestBase.java @@ -145,12 +145,13 @@ public void setUp() throws Exception { new ConsensusCommitManager( storage, admin, - consensusCommitConfig, databaseConfig, coordinator, parallelExecutor, recoveryExecutor, commit, + consensusCommitConfig.getIsolation(), + false, groupCommitter); } diff --git a/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitSpecificIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitSpecificIntegrationTestBase.java index 2d69c658de..b07c2fa3f9 100644 --- a/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitSpecificIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitSpecificIntegrationTestBase.java @@ -25,7 +25,6 @@ import com.scalar.db.api.DistributedStorage; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.DistributedTransaction; -import com.scalar.db.api.DistributedTransactionManager; import com.scalar.db.api.Get; import com.scalar.db.api.Insert; import com.scalar.db.api.Put; @@ -39,6 +38,7 @@ import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; +import com.scalar.db.common.DecoratedDistributedTransaction; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.storage.ExecutionException; import com.scalar.db.exception.transaction.CommitConflictException; @@ -54,7 +54,6 @@ import com.scalar.db.io.Key; import com.scalar.db.io.Value; import com.scalar.db.service.StorageFactory; -import com.scalar.db.service.TransactionFactory; import com.scalar.db.transaction.consensuscommit.CoordinatorGroupCommitter.CoordinatorGroupCommitKeyManipulator; import com.scalar.db.util.groupcommit.GroupCommitKeyManipulator.Keys; import java.time.Duration; @@ -74,6 +73,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.condition.EnabledIf; @@ -82,6 +82,7 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +@SuppressWarnings("UseCorrectAssertInTests") @TestInstance(TestInstance.Lifecycle.PER_CLASS) public abstract class ConsensusCommitSpecificIntegrationTestBase { @@ -110,7 +111,6 @@ public abstract class ConsensusCommitSpecificIntegrationTestBase { private String namespace2; private ParallelExecutor parallelExecutor; - private ConsensusCommitManager manager; private DistributedStorage storage; private Coordinator coordinator; private RecoveryHandler recovery; @@ -119,7 +119,7 @@ public abstract class ConsensusCommitSpecificIntegrationTestBase { @Nullable private CoordinatorGroupCommitter groupCommitter; @BeforeAll - public void beforeAll() throws Exception { + void beforeAll() throws Exception { initialize(); Properties properties = getProperties(TEST_NAME); @@ -174,38 +174,8 @@ protected Map getCreationOptions() { } @BeforeEach - public void setUp() throws Exception { + void setUp() throws Exception { truncateTables(); - storage = spy(originalStorage); - coordinator = spy(new Coordinator(storage, consensusCommitConfig)); - TransactionTableMetadataManager tableMetadataManager = - new TransactionTableMetadataManager(admin, -1); - recovery = spy(new RecoveryHandler(storage, coordinator, tableMetadataManager)); - recoveryExecutor = new RecoveryExecutor(coordinator, recovery, tableMetadataManager); - groupCommitter = CoordinatorGroupCommitter.from(consensusCommitConfig).orElse(null); - commit = spy(createCommitHandler(tableMetadataManager, groupCommitter)); - manager = - new ConsensusCommitManager( - storage, - admin, - consensusCommitConfig, - databaseConfig, - coordinator, - parallelExecutor, - recoveryExecutor, - commit, - groupCommitter); - } - - private CommitHandler createCommitHandler( - TransactionTableMetadataManager tableMetadataManager, - @Nullable CoordinatorGroupCommitter groupCommitter) { - if (groupCommitter != null) { - return new CommitHandlerWithGroupCommit( - storage, coordinator, tableMetadataManager, parallelExecutor, true, groupCommitter); - } else { - return new CommitHandler(storage, coordinator, tableMetadataManager, parallelExecutor, true); - } } @AfterEach @@ -223,7 +193,7 @@ private void truncateTables() throws ExecutionException { } @AfterAll - public void afterAll() throws Exception { + void afterAll() throws Exception { dropTables(); consensusCommitAdmin.close(); originalStorage.close(); @@ -239,10 +209,37 @@ private void dropTables() throws ExecutionException { } @Test - public void get_GetGivenForCommittedRecord_ShouldReturnRecord() throws TransactionException { + void begin_CorrectTransactionIdGiven_ShouldNotThrowAnyExceptions() { // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SNAPSHOT); + + // Act Assert + assertThatCode( + () -> { + DistributedTransaction transaction = manager.begin(ANY_ID_1); + transaction.commit(); + }) + .doesNotThrowAnyException(); + } + + @Test + void begin_EmptyTransactionIdGiven_ShouldThrowIllegalArgumentException() { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SNAPSHOT); + + // Act Assert + assertThatThrownBy(() -> manager.begin("")).isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void get_GetGivenForCommittedRecord_ShouldReturnRecord(Isolation isolation, boolean readOnly) + throws TransactionException, CoordinatorException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + reset(coordinator); + DistributedTransaction transaction = begin(manager, readOnly); Get get = prepareGet(0, 0, namespace1, TABLE_1); // Act @@ -251,218 +248,1001 @@ public void get_GetGivenForCommittedRecord_ShouldReturnRecord() throws Transacti // Assert assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) .isEqualTo(TransactionState.COMMITTED); + + // commit-state should not occur for read-only transactions + verify(coordinator, never()).putState(any(Coordinator.State.class)); } - @Test - public void scan_ScanGivenForCommittedRecord_ShouldReturnRecord() throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); - Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void scan_ScanGivenForCommittedRecords_ShouldReturnRecords(Isolation isolation, boolean readOnly) + throws TransactionException, CoordinatorException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + reset(coordinator); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); // Act List results = transaction.scan(scan); transaction.commit(); // Assert - assertThat(results.size()).isEqualTo(1); + assertThat(results.size()).isEqualTo(4); + assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(results.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); assertThat( ((TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult()).getState()) .isEqualTo(TransactionState.COMMITTED); + assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) results.get(1)).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(results.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) results.get(2)).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(results.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(results.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(results.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) results.get(3)).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + // commit-state should not occur for read-only transactions + verify(coordinator, never()).putState(any(Coordinator.State.class)); } - @Test - public void get_CalledTwice_ShouldReturnFromSnapshotInSecondTime() + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void getScanner_ScanGivenForCommittedRecords_ShouldReturnRecords( + Isolation isolation, boolean readOnly) throws TransactionException, CoordinatorException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + reset(coordinator); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); + + // Act Assert + TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); + + Optional result = scanner.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + assertThat(scanner.one()).isEmpty(); + + scanner.close(); + transaction.commit(); + + // commit-state should not occur for read-only transactions + verify(coordinator, never()).putState(any(Coordinator.State.class)); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void get_CalledTwice_ShouldBehaveCorrectly(Isolation isolation, boolean readOnly) throws TransactionException, ExecutionException { // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); Get get = prepareGet(0, 0, namespace1, TABLE_1); // Act Optional result1 = transaction.get(get); Optional result2 = transaction.get(get); + transaction.commit(); // Assert - verify(storage).get(any(Get.class)); + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation, the storage operation should be called twice because every get + // should be a new read + verify(storage, times(2)).get(any(Get.class)); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation, the storage operation should be called once because the second get + // should be served from the snapshot + verify(storage).get(any(Get.class)); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, the storage operation should be called twice because the second + // get should be served from the snapshot, but the storage operation should additionally be + // call the by the validation + verify(storage, times(2)).get(any(Get.class)); + } + + assertThat(result1.isPresent()).isTrue(); + assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(result1).isEqualTo(result2); } - @Test - public void - get_CalledTwiceAndAnotherTransactionCommitsInBetween_ShouldReturnFromSnapshotInSecondTime() - throws TransactionException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void get_CalledTwiceAndAnotherTransactionCommitsInBetween_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException, ExecutionException { // Arrange - DistributedTransaction transaction = manager.begin(); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); Get get = prepareGet(0, 0, namespace1, TABLE_1); // Act Optional result1 = transaction.get(get); - populateRecords(namespace1, TABLE_1); + + // The record updated by another transaction + int updatedBalance = 100; + manager.update( + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, updatedBalance) + .build()); + Optional result2 = transaction.get(get); - transaction.commit(); // Assert - verify(storage).get(any(Get.class)); - assertThat(result1).isEqualTo(result2); - } + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation - @Test - public void get_GetGivenForNonExisting_ShouldReturnEmpty() throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); - Get get = prepareGet(0, 4, namespace1, TABLE_1); + transaction.commit(); - // Act - Optional result = transaction.get(get); - transaction.commit(); + // The storage operation should be called three times: twice for the get by this transaction + // and once for the update by another transaction + verify(storage, times(3)).get(any(Get.class)); - // Assert - assertThat(result.isPresent()).isFalse(); + // The second record should be the updated one + assertThat(result1.isPresent()).isTrue(); + assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + assertThat(result2.isPresent()).isTrue(); + assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result2.get().getInt(BALANCE)).isEqualTo(updatedBalance); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation + + transaction.commit(); + + // The storage operation should be called twice: once for the get by this transaction and + // once for the update by another transaction + verify(storage, times(2)).get(any(Get.class)); + + // The first record should be the same as the second because the second + // should be returned from the snapshot + assertThat(result1.isPresent()).isTrue(); + assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + assertThat(result1).isEqualTo(result2); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } } - @Test - public void scan_ScanGivenForNonExisting_ShouldReturnEmpty() throws TransactionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void scan_CalledTwice_ShouldBehaveCorrectly(Isolation isolation, boolean readOnly) + throws TransactionException, ExecutionException { // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); - Scan scan = prepareScan(0, 4, 4, namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); // Act - List results = transaction.scan(scan); + List result1 = transaction.scan(scan); + List result2 = transaction.scan(scan); transaction.commit(); // Assert - assertThat(results.size()).isEqualTo(0); - } + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation, the storage operation should be called twice because every + // scan should be a new read + verify(storage, times(2)).scan(any(Scan.class)); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation, the storage operation should be called once because the second scan + // should be served from the snapshot + verify(storage).scan(any(Scan.class)); + } else { + assert isolation == Isolation.SERIALIZABLE; - public enum CommitType { - NORMAL_COMMIT, - GROUP_COMMIT, - DELAYED_GROUP_COMMIT - } + // In SERIALIZABLE isolation, the storage operation should be called twice because the second + // scan should be served from the snapshot, but the storage operation should additionally be + // call the by the validation + verify(storage, times(2)).scan(any(Scan.class)); + } - static Stream commitTypeAndIsolation() { - return Arrays.stream(CommitType.values()) - .flatMap( - commitType -> - Arrays.stream(Isolation.values()) - .map(isolation -> Arguments.of(commitType, isolation))); + assertThat(result1).hasSize(4); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(0)).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(2)).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result1.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(3)).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + assertThat(result1).isEqualTo(result2); } - private void selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) - throws ExecutionException, CoordinatorException, TransactionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void scan_CalledTwiceAndAnotherTransactionUpdatesRecordInBetween_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException, ExecutionException { // Arrange - long current = System.currentTimeMillis(); - String ongoingTxId = - populatePreparedRecordAndCoordinatorStateRecord( - storage, - namespace1, - TABLE_1, - TransactionState.PREPARED, - current, - TransactionState.COMMITTED, - commitType); - DistributedTransaction transaction = manager.begin(isolation); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); // Act - TransactionResult result; - if (s instanceof Get) { - Optional r = transaction.get((Get) s); - assertThat(r).isPresent(); - result = (TransactionResult) ((FilteredResult) r.get()).getOriginalResult(); - } else { - List results; - if (!useScanner) { - results = transaction.scan((Scan) s); - } else { - try (TransactionCrudOperable.Scanner scanner = transaction.getScanner((Scan) s)) { - results = scanner.all(); - } - } - assertThat(results.size()).isEqualTo(1); - result = (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); - } - - assertThat(result.getId()).isEqualTo(ongoingTxId); - assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); - assertThat(result.getVersion()).isEqualTo(2); - assertThat(result.getCommittedAt()).isGreaterThan(0); + List result1 = transaction.scan(scan); - transaction.commit(); + // The record updated by another transaction + int updatedBalance = 100; + manager.update( + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, updatedBalance) + .build()); - // Wait for the recovery to complete - ((ConsensusCommit) transaction).getCrudHandler().waitForRecoveryCompletion(); + List result2 = transaction.scan(scan); // Assert - verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void get_GetGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - get, commitType, isolation, false); - } + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + transaction.commit(); + + // The storage operation should be called twice because every scan should be a new read + verify(storage, times(2)).scan(any(Scan.class)); + + assertThat(result1).hasSize(4); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result1.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(3)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + + // The update record should be returned by the second scan + assertThat(result2).hasSize(4); + assertThat(result2.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result2.get(0).getInt(BALANCE)).isEqualTo(100); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result2.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result2.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result2.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(3)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation + + transaction.commit(); + + // The storage operation should be called once because the second scan should be served from + // the snapshot + verify(storage).scan(any(Scan.class)); + + assertThat(result1).hasSize(4); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result1.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(3)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + + // The same result should as the first scan should be returned by the second scan + assertThat(result1).isEqualTo(result2); + } else { + assert isolation == Isolation.SERIALIZABLE; - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scan_ScanGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); } - Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - scan, commitType, isolation, false); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; + @MethodSource("isolationAndReadOnlyMode") + void scan_CalledTwiceAndAnotherTransactionInsertsRecordInBetween_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException, ExecutionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + manager.mutate( + Arrays.asList( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, INITIAL_BALANCE) + .build(), + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) + .intValue(BALANCE, INITIAL_BALANCE) + .build())); + + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); + + // Act + List result1 = transaction.scan(scan); + + // The record inserted by another transaction + manager.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + + List result2 = transaction.scan(scan); + + // Assert + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + transaction.commit(); + + // The storage operation should be called twice because every scan should be a new read + verify(storage, times(2)).scan(any(Scan.class)); + + assertThat(result1).hasSize(2); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // The inserted record should be returned by the second scan + assertThat(result2).hasSize(3); + assertThat(result2.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result2.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result2.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result2.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation + + transaction.commit(); + + // The storage operation should be called once because the second scan should be served from + // the snapshot + verify(storage).scan(any(Scan.class)); + + assertThat(result1).hasSize(2); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // The same result should as the first scan should be returned by the second scan + assertThat(result1).isEqualTo(result2); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); } - Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - scan, commitType, isolation, true); } - private void selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) - throws ExecutionException, CoordinatorException, TransactionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void getScanner_CalledTwice_ShouldBehaveCorrectly(Isolation isolation, boolean readOnly) + throws TransactionException, ExecutionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); + + // Act Assert + TransactionCrudOperable.Scanner scanner1 = transaction.getScanner(scan); + + Optional result = scanner1.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner1.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner1.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner1.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + assertThat(scanner1.one()).isEmpty(); + + scanner1.close(); + + TransactionCrudOperable.Scanner scanner2 = transaction.getScanner(scan); + + result = scanner2.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner2.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner2.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + result = scanner2.one(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) + .isEqualTo(TransactionState.COMMITTED); + + assertThat(scanner2.one()).isEmpty(); + + scanner2.close(); + + transaction.commit(); + + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation, the storage operation should be called twice because every + // scan should be a new read + verify(storage, times(2)).scan(any(Scan.class)); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation, the storage operation should be called once because the second scan + // should be served from the snapshot + verify(storage).scan(any(Scan.class)); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, the storage operation should be called twice because the second + // scan should be served from the snapshot, but the storage operation should additionally be + // call the by the validation + verify(storage, times(2)).scan(any(Scan.class)); + } + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void getScanner_CalledTwiceAndAnotherTransactionUpdateRecordInBetween_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException, ExecutionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); + + // Act + TransactionCrudOperable.Scanner scanner1 = transaction.getScanner(scan); + List result1 = scanner1.all(); + scanner1.close(); + + // The record updated by another transaction + int updatedBalance = 100; + manager.update( + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, updatedBalance) + .build()); + + TransactionCrudOperable.Scanner scanner2 = transaction.getScanner(scan); + List result2 = scanner2.all(); + scanner2.close(); + + // Assert + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + transaction.commit(); + + // The storage operation should be called twice because every scan should be a new read + verify(storage, times(2)).scan(any(Scan.class)); + + assertThat(result1).hasSize(4); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result1.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(3)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + + // The update record should be returned by the second scan + assertThat(result2).hasSize(4); + assertThat(result2.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result2.get(0).getInt(BALANCE)).isEqualTo(100); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result2.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result2.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result2.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(3)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation + + transaction.commit(); + + // The storage operation should be called once because the second scan should be served from + // the snapshot + verify(storage).scan(any(Scan.class)); + + assertThat(result1).hasSize(4); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); + assertThat(result1.get(3).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(3)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + + // The same result should as the first scan should be returned by the second scan + assertThat(result1).isEqualTo(result2); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void getScanner_CalledTwiceAndAnotherTransactionInsertsRecordInBetween_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException, ExecutionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + manager.mutate( + Arrays.asList( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, INITIAL_BALANCE) + .build(), + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) + .intValue(BALANCE, INITIAL_BALANCE) + .build())); + + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, namespace1, TABLE_1); + + // Act + TransactionCrudOperable.Scanner scanner1 = transaction.getScanner(scan); + List result1 = scanner1.all(); + scanner1.close(); + + // The record inserted by another transaction + manager.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + + TransactionCrudOperable.Scanner scanner2 = transaction.getScanner(scan); + List result2 = scanner2.all(); + scanner2.close(); + + // Assert + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + transaction.commit(); + + // The storage operation should be called twice because every scan should be a new read + verify(storage, times(2)).scan(any(Scan.class)); + + assertThat(result1).hasSize(2); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // The inserted record should be returned by the second scan + assertThat(result2).hasSize(3); + assertThat(result2.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result2.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(0)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result2.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result2.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result2.get(2).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result2.get(2)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation + + transaction.commit(); + + // The storage operation should be called once because the second scan should be served from + // the snapshot + verify(storage).scan(any(Scan.class)); + + assertThat(result1).hasSize(2); + assertThat(result1.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + assertThat( + ((TransactionResult) ((FilteredResult) result1.get(1)).getOriginalResult()) + .getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(result1.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result1.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + // The same result should as the first scan should be returned by the second scan + assertThat(result1).isEqualTo(result2); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void get_GetGivenForNonExisting_ShouldReturnEmpty(Isolation isolation, boolean readOnly) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Get get = prepareGet(0, 4, namespace1, TABLE_1); + + // Act + Optional result = transaction.get(get); + transaction.commit(); + + // Assert + assertThat(result.isPresent()).isFalse(); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void scan_ScanGivenForNonExisting_ShouldReturnEmpty(Isolation isolation, boolean readOnly) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, 4, 4, namespace1, TABLE_1); + + // Act + List results = transaction.scan(scan); + transaction.commit(); + + // Assert + assertThat(results.size()).isEqualTo(0); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void getScanner_ScanGivenForNonExisting_ShouldReturnEmpty(Isolation isolation, boolean readOnly) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Scan scan = prepareScan(0, 4, 4, namespace1, TABLE_1); + + // Act Assert + TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); + assertThat(scanner.one()).isEmpty(); + scanner.close(); + transaction.commit(); + } + + private void + selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Selection s, + boolean useScanner, + Isolation isolation, + boolean readOnly, + CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long current = System.currentTimeMillis(); - populatePreparedRecordAndCoordinatorStateRecord( - storage, - namespace1, - TABLE_1, - TransactionState.PREPARED, - current, - TransactionState.ABORTED, - commitType); - DistributedTransaction transaction = manager.begin(isolation); + String ongoingTxId = + populatePreparedRecordAndCoordinatorStateRecord( + storage, + namespace1, + TABLE_1, + TransactionState.PREPARED, + current, + TransactionState.COMMITTED, + commitType); + DistributedTransaction transaction = begin(manager, readOnly); // Act TransactionResult result; @@ -482,139 +1262,370 @@ private void selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_Shou assertThat(results.size()).isEqualTo(1); result = (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); } - - assertThat(result.getId()).isEqualTo(ANY_ID_1); - assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); - assertThat(result.getVersion()).isEqualTo(1); - assertThat(result.getCommittedAt()).isEqualTo(1); - transaction.commit(); - // Wait for the recovery to complete - ((ConsensusCommit) transaction).getCrudHandler().waitForRecoveryCompletion(); + waitForRecoveryCompletion(transaction); // Assert - verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + // The rolled backed record should be returned + assertThat(result.getId()).isEqualTo(ANY_ID_1); + assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); + assertThat(result.getVersion()).isEqualTo(1); + assertThat(result.getCommittedAt()).isEqualTo(1); + + if (readOnly) { + // In read-only mode, recovery should not occur + verify(recovery, never()) + .recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery, never()) + .rollforwardRecord(any(Selection.class), any(TransactionResult.class)); + } else { + // In read-write mode, recovery should occur + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); + } + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // The rolled forward record should be returned + assertThat(result.getId()).isEqualTo(ongoingTxId); + assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); + assertThat(result.getVersion()).isEqualTo(2); + assertThat(result.getCommittedAt()).isGreaterThan(0); + + // Recovery should occur + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); + } } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void get_GetGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) - throws TransactionException, ExecutionException, CoordinatorException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - get, commitType, isolation, false); + selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scan_ScanGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) - throws TransactionException, ExecutionException, CoordinatorException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - scan, commitType, isolation, false); + selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) - throws TransactionException, ExecutionException, CoordinatorException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - scan, commitType, isolation, true); + selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); } - private void - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) - throws ExecutionException, CoordinatorException, TransactionException { - // Arrange - long prepared_at = System.currentTimeMillis(); - populatePreparedRecordAndCoordinatorStateRecord( - storage, namespace1, TABLE_1, TransactionState.PREPARED, prepared_at, null, commitType); - DistributedTransaction transaction = manager.begin(isolation); - + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scanAll_ScanAllGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanAllGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); + } + + private void selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Selection s, boolean useScanner, Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + long current = System.currentTimeMillis(); + populatePreparedRecordAndCoordinatorStateRecord( + storage, + namespace1, + TABLE_1, + TransactionState.PREPARED, + current, + TransactionState.ABORTED, + commitType); + DistributedTransaction transaction = begin(manager, readOnly); + // Act - assertThatThrownBy( - () -> { - if (s instanceof Get) { - transaction.get((Get) s); - } else { - if (!useScanner) { - transaction.scan((Scan) s); + TransactionResult result; + if (s instanceof Get) { + Optional r = transaction.get((Get) s); + assertThat(r).isPresent(); + result = (TransactionResult) ((FilteredResult) r.get()).getOriginalResult(); + } else { + List results; + if (!useScanner) { + results = transaction.scan((Scan) s); + } else { + try (TransactionCrudOperable.Scanner scanner = transaction.getScanner((Scan) s)) { + results = scanner.all(); + } + } + assertThat(results.size()).isEqualTo(1); + result = (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); + } + + transaction.commit(); + + waitForRecoveryCompletion(transaction); + + // Assert + + // In all isolations, the rolled back record should be returned + assertThat(result.getId()).isEqualTo(ANY_ID_1); + assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); + assertThat(result.getVersion()).isEqualTo(1); + assertThat(result.getCommittedAt()).isEqualTo(1); + + if (isolation == Isolation.READ_COMMITTED && readOnly) { + // In READ_COMMITTED isolation and read-only mode, recovery should not occur + verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } else { + // In other cases, recovery should occur + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws TransactionException, ExecutionException, CoordinatorException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + Get get = prepareGet(0, 0, namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws TransactionException, ExecutionException, CoordinatorException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws TransactionException, ExecutionException, CoordinatorException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scanAll_ScanAllGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws TransactionException, ExecutionException, CoordinatorException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanAllGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws TransactionException, ExecutionException, CoordinatorException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); + } + + private void + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Selection s, + boolean useScanner, + Isolation isolation, + boolean readOnly, + CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + long prepared_at = System.currentTimeMillis(); + populatePreparedRecordAndCoordinatorStateRecord( + storage, namespace1, TABLE_1, TransactionState.PREPARED, prepared_at, null, commitType); + DistributedTransaction transaction = begin(manager, readOnly); + + // Act Assert + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + // UncommittedRecordException should not be thrown + assertThatCode( + () -> { + TransactionResult result; + if (s instanceof Get) { + Optional r = transaction.get((Get) s); + assertThat(r).isPresent(); + result = (TransactionResult) ((FilteredResult) r.get()).getOriginalResult(); } else { - try (TransactionCrudOperable.Scanner scanner = transaction.getScanner((Scan) s)) { - scanner.all(); + List results; + if (!useScanner) { + results = transaction.scan((Scan) s); + } else { + try (TransactionCrudOperable.Scanner scanner = + transaction.getScanner((Scan) s)) { + results = scanner.all(); + } } + assertThat(results.size()).isEqualTo(1); + result = + (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); } - } - }) - .isInstanceOf(UncommittedRecordException.class); - transaction.rollback(); + // The rolled back record should be returned + assertThat(result.getId()).isEqualTo(ANY_ID_1); + assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); + assertThat(result.getVersion()).isEqualTo(1); + assertThat(result.getCommittedAt()).isEqualTo(1); + }) + .doesNotThrowAnyException(); - // Assert + transaction.commit(); + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // UncommittedRecordException should be thrown + assertThatThrownBy( + () -> { + if (s instanceof Get) { + transaction.get((Get) s); + } else { + if (!useScanner) { + transaction.scan((Scan) s); + } else { + try (TransactionCrudOperable.Scanner scanner = + transaction.getScanner((Scan) s)) { + scanner.all(); + } + } + } + }) + .isInstanceOf(UncommittedRecordException.class); + + transaction.rollback(); + } + + // In all cases, recovery should not occur verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); verify(coordinator, never()).putState(any(Coordinator.State.class)); + verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - get_GetGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - get, commitType, isolation, false); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - scan_ScanGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scan, commitType, isolation, false); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + getScanner_ScanGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -622,20 +1633,57 @@ public void getScanner_ScanGivenForPreparedWhenCoordinatorStateAborted_ShouldRol return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scan, commitType, isolation, true); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + scanAll_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + getScanner_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); } private void - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Selection s, + boolean useScanner, + Isolation isolation, + boolean readOnly, + CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long prepared_at = System.currentTimeMillis() - RecoveryHandler.TRANSACTION_LIFETIME_MILLIS - 1; String ongoingTxId = populatePreparedRecordAndCoordinatorStateRecord( storage, namespace1, TABLE_1, TransactionState.PREPARED, prepared_at, null, commitType); - DistributedTransaction transaction = manager.begin(isolation); + DistributedTransaction transaction = begin(manager, readOnly); // Act TransactionResult result; @@ -656,26 +1704,36 @@ public void getScanner_ScanGivenForPreparedWhenCoordinatorStateAborted_ShouldRol result = (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); } + transaction.commit(); + + waitForRecoveryCompletion(transaction); + + // Assert + + // In all isolations, the rolled back record should be returned assertThat(result.getId()).isEqualTo(ANY_ID_1); assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); assertThat(result.getVersion()).isEqualTo(1); assertThat(result.getCommittedAt()).isEqualTo(1); - transaction.commit(); - - // Wait for the recovery to complete - ((ConsensusCommit) transaction).getCrudHandler().waitForRecoveryCompletion(); + if (isolation == Isolation.READ_COMMITTED && readOnly) { + // In READ_COMMITTED isolation and read-only mode, recovery should not occur + verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(coordinator, never()).putState(any(Coordinator.State.class)); + verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } else { + // In other cases, recovery should occur - // Assert - verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(coordinator).putState(new Coordinator.State(ongoingTxId, TransactionState.ABORTED)); - verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(coordinator).putState(new Coordinator.State(ongoingTxId, TransactionState.ABORTED)); + verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void get_GetGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -683,46 +1741,81 @@ public void get_GetGivenForPreparedWhenCoordinatorStateNotExistAndExpired_Should return; } Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - get, commitType, isolation, false); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - scan_ScanGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scan, commitType, isolation, false); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scan, commitType, isolation, true); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + getScanner_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); } - private void selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scanAll_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + private void + selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Selection s, + boolean useScanner, + Isolation isolation, + boolean readOnly, + CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long current = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, @@ -732,11 +1825,15 @@ private void selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_Sho current, TransactionState.COMMITTED, commitType); - DistributedTransaction transaction = manager.begin(isolation); + DistributedTransaction transaction = begin(manager, readOnly); // Act + @Nullable TransactionResult result; if (s instanceof Get) { - assertThat(transaction.get((Get) s).isPresent()).isFalse(); + Optional r = transaction.get((Get) s); + result = + r.map(value -> (TransactionResult) ((FilteredResult) value).getOriginalResult()) + .orElse(null); } else { List results; if (!useScanner) { @@ -746,23 +1843,57 @@ private void selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_Sho results = scanner.all(); } } - assertThat(results.size()).isEqualTo(0); + + if (!results.isEmpty()) { + assertThat(results.size()).isEqualTo(1); + result = (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); + } else { + result = null; + } } transaction.commit(); - // Wait for the recovery to complete - ((ConsensusCommit) transaction).getCrudHandler().waitForRecoveryCompletion(); + waitForRecoveryCompletion(transaction); // Assert - verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + // The rolled back record should be returned + assertThat(result).isNotNull(); + assertThat(result.getId()).isEqualTo(ANY_ID_1); + assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); + assertThat(result.getVersion()).isEqualTo(1); + assertThat(result.getCommittedAt()).isEqualTo(1); + + if (readOnly) { + // In read-only mode, recovery should not occur + verify(recovery, never()) + .recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery, never()) + .rollforwardRecord(any(Selection.class), any(TransactionResult.class)); + } else { + // In read-write mode, recovery should occur + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); + } + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // The rolled forward record should be returned + assertThat(result).isNull(); + + // Recovery should occur + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); + } } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void get_GetGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -770,14 +1901,14 @@ public void get_GetGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforwar return; } Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - get, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scan_ScanGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -785,14 +1916,14 @@ public void scan_ScanGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforw return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - scan, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -800,14 +1931,45 @@ public void getScanner_ScanGivenForDeletedWhenCoordinatorStateCommitted_ShouldRo return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - scan, commitType, isolation, true); + selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scanAll_ScanAllGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanAllGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); } - private void selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) + private void selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Selection s, boolean useScanner, Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long current = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, @@ -817,7 +1979,7 @@ private void selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_Shoul current, TransactionState.ABORTED, commitType); - DistributedTransaction transaction = manager.begin(isolation); + DistributedTransaction transaction = begin(manager, readOnly); // Act TransactionResult result; @@ -838,25 +2000,33 @@ private void selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_Shoul result = (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); } + transaction.commit(); + + waitForRecoveryCompletion(transaction); + + // Assert + + // In all isolations, the rolled back record should be returned assertThat(result.getId()).isEqualTo(ANY_ID_1); assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); assertThat(result.getVersion()).isEqualTo(1); assertThat(result.getCommittedAt()).isEqualTo(1); - transaction.commit(); - - // Wait for the recovery to complete - ((ConsensusCommit) transaction).getCrudHandler().waitForRecoveryCompletion(); - - // Assert - verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + if (isolation == Isolation.READ_COMMITTED && readOnly) { + // In READ_COMMITTED isolation and read-only mode, recovery should not occur + verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } else { + // In other cases, recovery should occur + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void get_GetGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -864,14 +2034,14 @@ public void get_GetGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( return; } Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - get, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scan_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -879,14 +2049,14 @@ public void scan_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - scan, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -894,82 +2064,156 @@ public void getScanner_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldRoll return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - scan, commitType, isolation, true); + selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scanAll_ScanAllGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanAllGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); } private void - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Selection s, + boolean useScanner, + Isolation isolation, + boolean readOnly, + CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long prepared_at = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, namespace1, TABLE_1, TransactionState.DELETED, prepared_at, null, commitType); - DistributedTransaction transaction = manager.begin(isolation); + DistributedTransaction transaction = begin(manager, readOnly); - // Act - assertThatThrownBy( - () -> { - if (s instanceof Get) { - transaction.get((Get) s); - } else { - if (!useScanner) { - transaction.scan((Scan) s); + // Act Assert + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + // UncommittedRecordException should not be thrown + assertThatCode( + () -> { + TransactionResult result; + if (s instanceof Get) { + Optional r = transaction.get((Get) s); + assertThat(r).isPresent(); + result = (TransactionResult) ((FilteredResult) r.get()).getOriginalResult(); } else { - try (TransactionCrudOperable.Scanner scanner = transaction.getScanner((Scan) s)) { - scanner.all(); + List results; + if (!useScanner) { + results = transaction.scan((Scan) s); + } else { + try (TransactionCrudOperable.Scanner scanner = + transaction.getScanner((Scan) s)) { + results = scanner.all(); + } } + assertThat(results.size()).isEqualTo(1); + result = + (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); } - } - }) - .isInstanceOf(UncommittedRecordException.class); - transaction.rollback(); + // The rolled back record should be returned + assertThat(result.getId()).isEqualTo(ANY_ID_1); + assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); + assertThat(result.getVersion()).isEqualTo(1); + assertThat(result.getCommittedAt()).isEqualTo(1); + }) + .doesNotThrowAnyException(); - // Assert + transaction.commit(); + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // UncommittedRecordException should be thrown + assertThatThrownBy( + () -> { + if (s instanceof Get) { + transaction.get((Get) s); + } else { + if (!useScanner) { + transaction.scan((Scan) s); + } else { + try (TransactionCrudOperable.Scanner scanner = + transaction.getScanner((Scan) s)) { + scanner.all(); + } + } + } + }) + .isInstanceOf(UncommittedRecordException.class); + + transaction.rollback(); + } + + // In all cases, recovery should not occur verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); verify(coordinator, never()).putState(any(Coordinator.State.class)); + verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - get_GetGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - get, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - scan_ScanGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scan, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + getScanner_ScanGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -977,20 +2221,57 @@ public void getScanner_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldRoll return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scan, commitType, isolation, true); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + scanAll_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + getScanner_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); } private void - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - Selection s, CommitType commitType, Isolation isolation, boolean useScanner) + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Selection s, + boolean useScanner, + Isolation isolation, + boolean readOnly, + CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long prepared_at = System.currentTimeMillis() - RecoveryHandler.TRANSACTION_LIFETIME_MILLIS - 1; String ongoingTxId = populatePreparedRecordAndCoordinatorStateRecord( storage, namespace1, TABLE_1, TransactionState.DELETED, prepared_at, null, commitType); - DistributedTransaction transaction = manager.begin(isolation); + DistributedTransaction transaction = begin(manager, readOnly); // Act TransactionResult result; @@ -1011,26 +2292,35 @@ public void getScanner_ScanGivenForDeletedWhenCoordinatorStateAborted_ShouldRoll result = (TransactionResult) ((FilteredResult) results.get(0)).getOriginalResult(); } - assertThat(result.getId()).isEqualTo(ANY_ID_1); - assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); - assertThat(result.getVersion()).isEqualTo(1); - assertThat(result.getCommittedAt()).isEqualTo(1); - transaction.commit(); // Wait for the recovery to complete - ((ConsensusCommit) transaction).getCrudHandler().waitForRecoveryCompletion(); + waitForRecoveryCompletion(transaction); // Assert - verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); - verify(coordinator).putState(new Coordinator.State(ongoingTxId, TransactionState.ABORTED)); - verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + + // In all isolations, the rolled back record should be returned + assertThat(result.getId()).isEqualTo(ANY_ID_1); + assertThat(result.getState()).isEqualTo(TransactionState.COMMITTED); + assertThat(result.getVersion()).isEqualTo(1); + assertThat(result.getCommittedAt()).isEqualTo(1); + + if (isolation == Isolation.READ_COMMITTED && readOnly) { + // In READ_COMMITTED isolation and read-only mode, recovery should not occur + verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } else { + // In other cases, recovery should occur + verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); + verify(coordinator).putState(new Coordinator.State(ongoingTxId, TransactionState.ABORTED)); + verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); + } } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void get_GetGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void get_GetGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -1038,14 +2328,14 @@ public void get_GetGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldA return; } Get get = prepareGet(0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - get, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + get, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scan_ScanGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scan_ScanGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. @@ -1053,39 +2343,69 @@ public void scan_ScanGivenForDeletedWhenCoordinatorStateNotExistAndExpired_Shoul return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scan, commitType, isolation, false); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scan, false, isolation, readOnly, commitType); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void getScanner_ScanGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scan, commitType, isolation, true); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scan, true, isolation, readOnly, commitType); } @ParameterizedTest - @EnumSource(CommitType.class) - public void - update_UpdateGivenForPreparedWhenCoordinatorStateCommitted_ShouldUpdateAfterRollforward( - CommitType commitType) + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void scanAll_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scanAll, false, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndReadOnlyModeAndCommitType") + void + getScanner_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly, CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { return; } + ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); + selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + scanAll, true, isolation, readOnly, commitType); + } + + @ParameterizedTest + @MethodSource("isolationAndCommitType") + void update_UpdateGivenForPreparedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { + // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature + // is disabled. Skip this test in this case. + if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { + return; + } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long current = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, @@ -1106,12 +2426,21 @@ public void scan_ScanGivenForDeletedWhenCoordinatorStateNotExistAndExpired_Shoul DistributedTransaction transaction = manager.begin(); - // Act + // Act Assert Optional result = transaction.get(get); assertThat(result.isPresent()).isTrue(); - assertThat(result.get().getInt(BALANCE)).isEqualTo(NEW_BALANCE); - int expectedBalance = result.get().getInt(BALANCE) + 100; + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + // The rolled back record should be returned + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // The rolled forward record should be returned + assertThat(result.get().getInt(BALANCE)).isEqualTo(NEW_BALANCE); + } transaction.update( Update.newBuilder() @@ -1119,24 +2448,43 @@ public void scan_ScanGivenForDeletedWhenCoordinatorStateNotExistAndExpired_Shoul .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expectedBalance) + .intValue(BALANCE, result.get().getInt(BALANCE) + 100) .build()); - transaction.commit(); + if (isolation == Isolation.READ_COMMITTED) { + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + transaction.rollback(); + } else { + // In SNAPSHOT or SERIALIZABLE isolation + // Should commit without any exceptions + transaction.commit(); + } - // Assert Optional actual = manager.get(get); assertThat(actual.isPresent()).isTrue(); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(expectedBalance); + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + // The transaction should not have committed + assertThat(actual.get().getInt(BALANCE)).isEqualTo(NEW_BALANCE); + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // The transaction should have committed + assertThat(actual.get().getInt(BALANCE)).isEqualTo(NEW_BALANCE + 100); + } + + // In all isolations, recovery should occur verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); } @ParameterizedTest - @EnumSource(CommitType.class) - public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdateAfterRollback( - CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndCommitType") + void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { @@ -1144,6 +2492,7 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long current = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, @@ -1164,39 +2513,40 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat DistributedTransaction transaction = manager.begin(); - // Act + // Act Assert Optional result = transaction.get(get); + + // In all isolations, the rolled back record should be returned assertThat(result.isPresent()).isTrue(); assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - int expectedBalance = result.get().getInt(BALANCE) + 100; - transaction.update( Update.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expectedBalance) + .intValue(BALANCE, result.get().getInt(BALANCE) + 100) .build()); transaction.commit(); - // Assert Optional actual = manager.get(get); + + // In all isolations, no anomalies occur assertThat(actual.isPresent()).isTrue(); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(expectedBalance); + assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE + 100); + // In all isolations, recovery should occur verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); } @ParameterizedTest - @EnumSource(CommitType.class) - public void - update_UpdateGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldThrowUncommittedRecordException( - CommitType commitType) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndCommitType") + void update_UpdateGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { @@ -1204,39 +2554,52 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long prepared_at = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, namespace1, TABLE_1, TransactionState.PREPARED, prepared_at, null, commitType); DistributedTransaction transaction = manager.begin(); - // Act - assertThatThrownBy( - () -> - transaction.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 100) - .build())) - .isInstanceOf(UncommittedRecordException.class); + Update update = + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 100) + .build(); - transaction.rollback(); + // Act Assert + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation - // Assert + // UncommittedRecordException should not be thrown when trying to update + assertThatCode(() -> transaction.update(update)).doesNotThrowAnyException(); + + // CommitConflictException should be thrown at the commit phase + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // UncommittedRecordException should be thrown when trying to update + assertThatThrownBy(() -> transaction.update(update)) + .isInstanceOf(UncommittedRecordException.class); + + transaction.rollback(); + } + + // In all isolations, recovery should not occur verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); verify(coordinator, never()).putState(any(Coordinator.State.class)); } @ParameterizedTest - @EnumSource(CommitType.class) - public void - update_UpdateGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldUpdateAfterAbortTransaction( - CommitType commitType) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndCommitType") + void update_UpdateGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { @@ -1244,6 +2607,7 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long prepared_at = System.currentTimeMillis() - RecoveryHandler.TRANSACTION_LIFETIME_MILLIS - 1; String ongoingTxId = populatePreparedRecordAndCoordinatorStateRecord( @@ -1259,40 +2623,41 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat DistributedTransaction transaction = manager.begin(); - // Act + // Act Assert Optional result = transaction.get(get); + + // In all isolations, the rolled back record should be returned assertThat(result.isPresent()).isTrue(); assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - int expectedBalance = result.get().getInt(BALANCE) + 100; - transaction.update( Update.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expectedBalance) + .intValue(BALANCE, result.get().getInt(BALANCE) + 100) .build()); transaction.commit(); - // Assert Optional actual = manager.get(get); + + // In all isolations, no anomalies occur assertThat(actual.isPresent()).isTrue(); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(expectedBalance); + assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE + 100); + // In all isolations, recovery should occur verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); verify(coordinator).putState(new Coordinator.State(ongoingTxId, TransactionState.ABORTED)); verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); } @ParameterizedTest - @EnumSource(CommitType.class) - public void - insert_InsertGivenForDeletedWhenCoordinatorStateCommitted_ShouldInsertAfterRollforward( - CommitType commitType) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndCommitType") + void insert_InsertGivenForDeletedWhenCoordinatorStateCommitted_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { @@ -1300,6 +2665,7 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long current = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, @@ -1320,9 +2686,17 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat DistributedTransaction transaction = manager.begin(); - // Act + // Act Assert Optional result = transaction.get(get); - assertThat(result.isPresent()).isFalse(); + + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation, the rolled back record should be returned + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + } else { + // In SNAPSHOT or SERIALIZABLE isolation, no record should be returned + assertThat(result.isPresent()).isFalse(); + } int expectedBalance = 100; @@ -1337,19 +2711,22 @@ public void update_UpdateGivenForPreparedWhenCoordinatorStateAborted_ShouldUpdat transaction.commit(); - // Assert Optional actual = manager.get(get); + + // In all isolations, the inserted record should be returned assertThat(actual.isPresent()).isTrue(); assertThat(actual.get().getInt(BALANCE)).isEqualTo(expectedBalance); + // In all isolations, recovery should occur verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); verify(recovery).rollforwardRecord(any(Selection.class), any(TransactionResult.class)); } @ParameterizedTest - @EnumSource(CommitType.class) - public void update_UpdateGivenForDeletedWhenCoordinatorStateAborted_ShouldUpdateAfterRollback( - CommitType commitType) throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndCommitType") + void update_UpdateGivenForDeletedWhenCoordinatorStateAborted_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { @@ -1357,6 +2734,7 @@ public void update_UpdateGivenForDeletedWhenCoordinatorStateAborted_ShouldUpdate } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long current = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, @@ -1379,37 +2757,38 @@ public void update_UpdateGivenForDeletedWhenCoordinatorStateAborted_ShouldUpdate // Act Optional result = transaction.get(get); + + // In all isolations, the rolled back record should be returned assertThat(result.isPresent()).isTrue(); assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - int expectedBalance = result.get().getInt(BALANCE) + 100; - transaction.update( Update.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expectedBalance) + .intValue(BALANCE, result.get().getInt(BALANCE) + 100) .build()); transaction.commit(); - // Assert Optional actual = manager.get(get); + + // In all isolations, no anomalies occur assertThat(actual.isPresent()).isTrue(); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(expectedBalance); + assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE + 100); + // In all isolations, recovery should occur verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); } @ParameterizedTest - @EnumSource(CommitType.class) - public void - update_UpdateGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldThrowUncommittedRecordException( - CommitType commitType) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndCommitType") + void update_UpdateGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { @@ -1417,39 +2796,52 @@ public void update_UpdateGivenForDeletedWhenCoordinatorStateAborted_ShouldUpdate } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long prepared_at = System.currentTimeMillis(); populatePreparedRecordAndCoordinatorStateRecord( storage, namespace1, TABLE_1, TransactionState.DELETED, prepared_at, null, commitType); DistributedTransaction transaction = manager.begin(); - // Act - assertThatThrownBy( - () -> - transaction.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 100) - .build())) - .isInstanceOf(UncommittedRecordException.class); + Update update = + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 100) + .build(); - transaction.rollback(); + // Act Assert + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation - // Assert + // UncommittedRecordException should not be thrown when trying to update + assertThatCode(() -> transaction.update(update)).doesNotThrowAnyException(); + + // CommitConflictException should be thrown at the commit phase + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } else { + // In SNAPSHOT or SERIALIZABLE isolation + + // UncommittedRecordException should be thrown when trying to update + assertThatThrownBy(() -> transaction.update(update)) + .isInstanceOf(UncommittedRecordException.class); + + transaction.rollback(); + } + + // In all isolations, recovery should not occur verify(recovery, never()).recover(any(Selection.class), any(TransactionResult.class), any()); verify(recovery, never()).rollbackRecord(any(Selection.class), any(TransactionResult.class)); verify(coordinator, never()).putState(any(Coordinator.State.class)); } @ParameterizedTest - @EnumSource(CommitType.class) - public void - update_UpdateGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldUpdateAfterAbortTransaction( - CommitType commitType) - throws ExecutionException, CoordinatorException, TransactionException { + @MethodSource("isolationAndCommitType") + void update_UpdateGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldBehaveCorrectly( + Isolation isolation, CommitType commitType) + throws ExecutionException, CoordinatorException, TransactionException { // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature // is disabled. Skip this test in this case. if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { @@ -1457,6 +2849,7 @@ public void update_UpdateGivenForDeletedWhenCoordinatorStateAborted_ShouldUpdate } // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); long prepared_at = System.currentTimeMillis() - RecoveryHandler.TRANSACTION_LIFETIME_MILLIS - 1; String ongoingTxId = populatePreparedRecordAndCoordinatorStateRecord( @@ -1472,43 +2865,47 @@ public void update_UpdateGivenForDeletedWhenCoordinatorStateAborted_ShouldUpdate DistributedTransaction transaction = manager.begin(); - // Act + // Act Assert Optional result = transaction.get(get); + + // In all isolations, the rolled back record should be returned assertThat(result.isPresent()).isTrue(); assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - int expectedBalance = result.get().getInt(BALANCE) + 100; - transaction.update( Update.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expectedBalance) + .intValue(BALANCE, result.get().getInt(BALANCE) + 100) .build()); transaction.commit(); - // Assert Optional actual = manager.get(get); + + // In all isolations, no anomalies occur assertThat(actual.isPresent()).isTrue(); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(expectedBalance); + assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE + 100); + // In all isolations, recovery should occur verify(recovery).recover(any(Selection.class), any(TransactionResult.class), any()); verify(coordinator).putState(new Coordinator.State(ongoingTxId, TransactionState.ABORTED)); verify(recovery).rollbackRecord(any(Selection.class), any(TransactionResult.class)); } - @Test - public void getThenScanAndGet_CommitHappenedInBetween_OnlyGetShouldReadRepeatably() - throws TransactionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void getThenScanAndGet_CommitHappenedInBetween_BehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); transaction.commit(); - DistributedTransaction transaction1 = manager.begin(); + DistributedTransaction transaction1 = begin(manager, readOnly); Optional result1 = transaction1.get(prepareGet(0, 0, namespace1, TABLE_1)); DistributedTransaction transaction2 = manager.begin(); @@ -1516,21 +2913,50 @@ public void getThenScanAndGet_CommitHappenedInBetween_OnlyGetShouldReadRepeatabl transaction2.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 2)); transaction2.commit(); - // Act + // Act Assert Result result2 = transaction1.scan(prepareScan(0, 0, 0, namespace1, TABLE_1)).get(0); Optional result3 = transaction1.get(prepareGet(0, 0, namespace1, TABLE_1)); - transaction1.commit(); - // Assert - assertThat(result1).isPresent(); - assertThat(result1.get()).isNotEqualTo(result2); - assertThat(result2.getInt(BALANCE)).isEqualTo(2); - assertThat(result1).isEqualTo(result3); + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + transaction1.commit(); + + // Each operation reads the committed value + assertThat(result1).isPresent(); + assertThat(result1.get().getInt(BALANCE)).isEqualTo(1); + + assertThat(result2.getInt(BALANCE)).isEqualTo(2); + + assertThat(result3).isPresent(); + assertThat(result3.get().getInt(BALANCE)).isEqualTo(2); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT or SERIALIZABLE isolation + + transaction1.commit(); + + // Only get reads repeatably + assertThat(result1).isPresent(); + assertThat(result1.get().getInt(BALANCE)).isEqualTo(1); + assertThat(result3).isPresent(); + assertThat(result3.get().getInt(BALANCE)).isEqualTo(1); + + // Scan reads the committed value + assertThat(result2.getInt(BALANCE)).isEqualTo(2); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } } - @Test - public void putAndCommit_PutGivenForNonExisting_ShouldCreateRecord() throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutGivenForNonExisting_ShouldCreateRecord(Isolation isolation) + throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); int expected = INITIAL_BALANCE; Put put = preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, expected); DistributedTransaction transaction = manager.begin(); @@ -1541,7 +2967,7 @@ public void putAndCommit_PutGivenForNonExisting_ShouldCreateRecord() throws Tran // Assert Get get = prepareGet(0, 0, namespace1, TABLE_1); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional r = another.get(get); another.commit(); @@ -1552,10 +2978,12 @@ public void putAndCommit_PutGivenForNonExisting_ShouldCreateRecord() throws Tran assertThat(result.getVersion()).isEqualTo(1); } - @Test - public void putAndCommit_PutWithImplicitPreReadEnabledGivenForNonExisting_ShouldCreateRecord() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutWithImplicitPreReadEnabledGivenForNonExisting_ShouldCreateRecord( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); int expected = INITIAL_BALANCE; Put put = Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)) @@ -1570,7 +2998,7 @@ public void putAndCommit_PutWithImplicitPreReadEnabledGivenForNonExisting_Should // Assert Get get = prepareGet(0, 0, namespace1, TABLE_1); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional r = another.get(get); another.commit(); @@ -1581,11 +3009,13 @@ public void putAndCommit_PutWithImplicitPreReadEnabledGivenForNonExisting_Should assertThat(result.getVersion()).isEqualTo(1); } - @Test - public void putAndCommit_PutGivenForExistingAfterRead_ShouldUpdateRecord() + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutGivenForExistingAfterRead_ShouldUpdateRecord(Isolation isolation) throws TransactionException, ExecutionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); Get get = prepareGet(0, 0, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); @@ -1600,7 +3030,7 @@ public void putAndCommit_PutGivenForExistingAfterRead_ShouldUpdateRecord() // Assert verify(storage).get(any(Get.class)); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional r = another.get(get); another.commit(); @@ -1611,11 +3041,13 @@ public void putAndCommit_PutGivenForExistingAfterRead_ShouldUpdateRecord() assertThat(actual.getVersion()).isEqualTo(2); } - @Test - public void putAndCommit_PutWithImplicitPreReadEnabledGivenForExisting_ShouldUpdateRecord() - throws TransactionException, ExecutionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutWithImplicitPreReadEnabledGivenForExisting_ShouldUpdateRecord( + Isolation isolation) throws TransactionException, ExecutionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); // Act @@ -1631,7 +3063,7 @@ public void putAndCommit_PutWithImplicitPreReadEnabledGivenForExisting_ShouldUpd // Assert verify(storage).get(any(Get.class)); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional r = another.get(prepareGet(0, 0, namespace1, TABLE_1)); another.commit(); @@ -1642,11 +3074,13 @@ public void putAndCommit_PutWithImplicitPreReadEnabledGivenForExisting_ShouldUpd assertThat(actual.getVersion()).isEqualTo(2); } - @Test - public void putAndCommit_PutGivenForExisting_ShouldThrowCommitConflictException() + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutGivenForExisting_ShouldThrowCommitConflictException(Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); // Act Assert @@ -1658,10 +3092,12 @@ public void putAndCommit_PutGivenForExisting_ShouldThrowCommitConflictException( assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); } - @Test - public void putAndCommit_PutWithInsertModeEnabledGivenForNonExisting_ShouldCreateRecord() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutWithInsertModeEnabledGivenForNonExisting_ShouldCreateRecord( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); int expected = INITIAL_BALANCE; Put put = Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)) @@ -1676,7 +3112,7 @@ public void putAndCommit_PutWithInsertModeEnabledGivenForNonExisting_ShouldCreat // Assert Get get = prepareGet(0, 0, namespace1, TABLE_1); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional r = another.get(get); another.commit(); @@ -1687,10 +3123,12 @@ public void putAndCommit_PutWithInsertModeEnabledGivenForNonExisting_ShouldCreat assertThat(result.getVersion()).isEqualTo(1); } - @Test - public void putAndCommit_PutWithInsertModeEnabledGivenForNonExistingAfterRead_ShouldCreateRecord() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutWithInsertModeEnabledGivenForNonExistingAfterRead_ShouldCreateRecord( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); Get get = prepareGet(0, 0, namespace1, TABLE_1); int expected = INITIAL_BALANCE; @@ -1710,7 +3148,7 @@ public void putAndCommit_PutWithInsertModeEnabledGivenForNonExistingAfterRead_Sh transaction.commit(); // Assert - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional r = another.get(get); another.commit(); @@ -1721,12 +3159,13 @@ public void putAndCommit_PutWithInsertModeEnabledGivenForNonExistingAfterRead_Sh assertThat(actual.getVersion()).isEqualTo(1); } - @Test - public void - putAndCommit_PutWithInsertModeGivenForExistingAfterRead_ShouldThrowCommitConflictException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_PutWithInsertModeGivenForExistingAfterRead_ShouldThrowCommitConflictException( + Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); Get get = prepareGet(0, 0, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); @@ -1743,68 +3182,20 @@ public void putAndCommit_PutWithInsertModeEnabledGivenForNonExistingAfterRead_Sh assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); } - @Test - public void - putAndCommit_SinglePartitionMutationsGiven_ShouldAccessStorageOnceForPrepareAndCommit() - throws TransactionException, ExecutionException, CoordinatorException { - // Arrange - IntValue balance = new IntValue(BALANCE, INITIAL_BALANCE); - List puts = preparePuts(namespace1, TABLE_1); - puts.get(0).withValue(balance); - puts.get(1).withValue(balance); - DistributedTransaction transaction = manager.begin(); - - // Act - transaction.put(puts.get(0)); - transaction.put(puts.get(1)); - transaction.commit(); - - // Assert - // one for prepare, one for commit - verify(storage, times(2)).mutate(anyList()); - if (isGroupCommitEnabled()) { - verify(coordinator) - .putStateForGroupCommit(anyString(), anyList(), any(TransactionState.class), anyLong()); - return; - } - verify(coordinator).putState(any(Coordinator.State.class)); - } - - @Test - public void putAndCommit_TwoPartitionsMutationsGiven_ShouldAccessStorageTwiceForPrepareAndCommit() - throws TransactionException, ExecutionException, CoordinatorException { - // Arrange - IntValue balance = new IntValue(BALANCE, INITIAL_BALANCE); - List puts = preparePuts(namespace1, TABLE_1); - puts.get(0).withValue(balance); - puts.get(NUM_TYPES).withValue(balance); // next account - DistributedTransaction transaction = manager.begin(); - - // Act - transaction.put(puts.get(0)); - transaction.put(puts.get(NUM_TYPES)); - transaction.commit(); - - // Assert - // twice for prepare, twice for commit - verify(storage, times(4)).mutate(anyList()); - if (isGroupCommitEnabled()) { - verify(coordinator) - .putStateForGroupCommit(anyString(), anyList(), any(TransactionState.class), anyLong()); - return; - } - verify(coordinator).putState(any(Coordinator.State.class)); - } - private void putAndCommit_GetsAndPutsGiven_ShouldCommitProperly( - String fromNamespace, String fromTable, String toNamespace, String toTable) + Isolation isolation, + String fromNamespace, + String fromTable, + String toNamespace, + String toTable) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); boolean differentTables = !fromNamespace.equals(toNamespace) || !fromTable.equals(toTable); - populateRecords(fromNamespace, fromTable); + populateRecords(manager, fromNamespace, fromTable); if (differentTables) { - populateRecords(toNamespace, toTable); + populateRecords(manager, toNamespace, toTable); } List fromGets = prepareGets(fromNamespace, fromTable); @@ -1817,10 +3208,11 @@ private void putAndCommit_GetsAndPutsGiven_ShouldCommitProperly( int to = NUM_TYPES; // Act - prepareTransfer(from, fromNamespace, fromTable, to, toNamespace, toTable, amount).commit(); + prepareTransfer(manager, from, fromNamespace, fromTable, to, toNamespace, toTable, amount) + .commit(); // Assert - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional fromResult = another.get(fromGets.get(from)); assertThat(fromResult).isPresent(); assertThat(fromResult.get().getValue(BALANCE)).isEqualTo(Optional.of(fromBalance)); @@ -1830,22 +3222,27 @@ private void putAndCommit_GetsAndPutsGiven_ShouldCommitProperly( another.commit(); } - @Test - public void putAndCommit_GetsAndPutsForSameTableGiven_ShouldCommitProperly() + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_GetsAndPutsForSameTableGiven_ShouldCommitProperly(Isolation isolation) throws TransactionException { - putAndCommit_GetsAndPutsGiven_ShouldCommitProperly(namespace1, TABLE_1, namespace1, TABLE_1); + putAndCommit_GetsAndPutsGiven_ShouldCommitProperly( + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void putAndCommit_GetsAndPutsForDifferentTablesGiven_ShouldCommitProperly() + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_GetsAndPutsForDifferentTablesGiven_ShouldCommitProperly(Isolation isolation) throws TransactionException { - putAndCommit_GetsAndPutsGiven_ShouldCommitProperly(namespace1, TABLE_1, namespace2, TABLE_2); + putAndCommit_GetsAndPutsGiven_ShouldCommitProperly( + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } private void commit_ConflictingPutsGivenForNonExisting_ShouldCommitOneAndAbortTheOther( - String namespace1, String table1, String namespace2, String table2) + Isolation isolation, String namespace1, String table1, String namespace2, String table2) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); boolean differentTables = !namespace1.equals(namespace2) || !table1.equals(table2); int expected = INITIAL_BALANCE; @@ -1884,7 +3281,7 @@ private void commit_ConflictingPutsGivenForNonExisting_ShouldCommitOneAndAbortTh // Assert verify(commit).rollbackRecords(any(Snapshot.class)); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); assertThat(another.get(gets1.get(from)).isPresent()).isFalse(); Optional toResult = another.get(gets2.get(to)); assertThat(toResult).isPresent(); @@ -1895,26 +3292,27 @@ private void commit_ConflictingPutsGivenForNonExisting_ShouldCommitOneAndAbortTh another.commit(); } - @Test - public void - commit_ConflictingPutsForSameTableGivenForNonExisting_ShouldCommitOneAndAbortTheOther() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_ConflictingPutsForSameTableGivenForNonExisting_ShouldCommitOneAndAbortTheOther( + Isolation isolation) throws TransactionException { commit_ConflictingPutsGivenForNonExisting_ShouldCommitOneAndAbortTheOther( - namespace1, TABLE_1, namespace1, TABLE_1); + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void - commit_ConflictingPutsForDifferentTablesGivenForNonExisting_ShouldCommitOneAndAbortTheOther() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_ConflictingPutsForDifferentTablesGivenForNonExisting_ShouldCommitOneAndAbortTheOther( + Isolation isolation) throws TransactionException { commit_ConflictingPutsGivenForNonExisting_ShouldCommitOneAndAbortTheOther( - namespace1, TABLE_1, namespace2, TABLE_2); + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } private void commit_ConflictingPutAndDeleteGivenForExisting_ShouldCommitPutAndAbortDelete( - String namespace1, String table1, String namespace2, String table2) + Isolation isolation, String namespace1, String table1, String namespace2, String table2) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); boolean differentTables = !namespace1.equals(namespace2) || !table1.equals(table2); int amount = 200; @@ -1923,9 +3321,9 @@ private void commit_ConflictingPutAndDeleteGivenForExisting_ShouldCommitPutAndAb int anotherFrom = to; int anotherTo = NUM_TYPES * 2; - populateRecords(namespace1, table1); + populateRecords(manager, namespace1, table1); if (differentTables) { - populateRecords(namespace2, table2); + populateRecords(manager, namespace2, table2); } DistributedTransaction transaction = manager.begin(); @@ -1942,7 +3340,14 @@ private void commit_ConflictingPutAndDeleteGivenForExisting_ShouldCommitPutAndAb assertThatCode( () -> prepareTransfer( - anotherFrom, namespace2, table2, anotherTo, namespace1, table1, amount) + manager, + anotherFrom, + namespace2, + table2, + anotherTo, + namespace1, + table1, + amount) .commit()) .doesNotThrowAnyException(); @@ -1950,7 +3355,7 @@ private void commit_ConflictingPutAndDeleteGivenForExisting_ShouldCommitPutAndAb // Assert verify(commit).rollbackRecords(any(Snapshot.class)); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional fromResult = another.get(gets1.get(from)); assertThat(fromResult).isPresent(); assertThat(getBalance(fromResult.get())).isEqualTo(INITIAL_BALANCE); @@ -1963,26 +3368,28 @@ private void commit_ConflictingPutAndDeleteGivenForExisting_ShouldCommitPutAndAb another.commit(); } - @Test - public void - commit_ConflictingPutAndDeleteForSameTableGivenForExisting_ShouldCommitPutAndAbortDelete() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_ConflictingPutAndDeleteForSameTableGivenForExisting_ShouldCommitPutAndAbortDelete( + Isolation isolation) throws TransactionException { commit_ConflictingPutAndDeleteGivenForExisting_ShouldCommitPutAndAbortDelete( - namespace1, TABLE_1, namespace1, TABLE_1); + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void - commit_ConflictingPutAndDeleteForDifferentTableGivenForExisting_ShouldCommitPutAndAbortDelete() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void + commit_ConflictingPutAndDeleteForDifferentTableGivenForExisting_ShouldCommitPutAndAbortDelete( + Isolation isolation) throws TransactionException { commit_ConflictingPutAndDeleteGivenForExisting_ShouldCommitPutAndAbortDelete( - namespace1, TABLE_1, namespace2, TABLE_2); + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } private void commit_ConflictingPutsGivenForExisting_ShouldCommitOneAndAbortTheOther( - String namespace1, String table1, String namespace2, String table2) + Isolation isolation, String namespace1, String table1, String namespace2, String table2) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); boolean differentTables = !namespace1.equals(namespace2) || !table1.equals(table2); int amount1 = 100; @@ -1992,19 +3399,26 @@ private void commit_ConflictingPutsGivenForExisting_ShouldCommitOneAndAbortTheOt int anotherFrom = to; int anotherTo = NUM_TYPES * 2; - populateRecords(namespace1, table1); + populateRecords(manager, namespace1, table1); if (differentTables) { - populateRecords(namespace2, table2); + populateRecords(manager, namespace2, table2); } DistributedTransaction transaction = - prepareTransfer(from, namespace1, table1, to, namespace2, table2, amount1); + prepareTransfer(manager, from, namespace1, table1, to, namespace2, table2, amount1); // Act Assert assertThatCode( () -> prepareTransfer( - anotherFrom, namespace2, table2, anotherTo, namespace1, table1, amount2) + manager, + anotherFrom, + namespace2, + table2, + anotherTo, + namespace1, + table1, + amount2) .commit()) .doesNotThrowAnyException(); @@ -2015,7 +3429,7 @@ private void commit_ConflictingPutsGivenForExisting_ShouldCommitOneAndAbortTheOt List gets1 = prepareGets(namespace1, table1); List gets2 = prepareGets(namespace2, table2); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional fromResult = another.get(gets1.get(from)); assertThat(fromResult).isPresent(); assertThat(getBalance(fromResult.get())).isEqualTo(INITIAL_BALANCE); @@ -2028,25 +3442,27 @@ private void commit_ConflictingPutsGivenForExisting_ShouldCommitOneAndAbortTheOt another.commit(); } - @Test - public void commit_ConflictingPutsForSameTableGivenForExisting_ShouldCommitOneAndAbortTheOther() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_ConflictingPutsForSameTableGivenForExisting_ShouldCommitOneAndAbortTheOther( + Isolation isolation) throws TransactionException { commit_ConflictingPutsGivenForExisting_ShouldCommitOneAndAbortTheOther( - namespace1, TABLE_1, namespace1, TABLE_1); + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void - commit_ConflictingPutsForDifferentTablesGivenForExisting_ShouldCommitOneAndAbortTheOther() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_ConflictingPutsForDifferentTablesGivenForExisting_ShouldCommitOneAndAbortTheOther( + Isolation isolation) throws TransactionException { commit_ConflictingPutsGivenForExisting_ShouldCommitOneAndAbortTheOther( - namespace1, TABLE_1, namespace2, TABLE_2); + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } private void commit_NonConflictingPutsGivenForExisting_ShouldCommitBoth( - String namespace1, String table1, String namespace2, String table2) + Isolation isolation, String namespace1, String table1, String namespace2, String table2) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); boolean differentTables = !namespace1.equals(namespace2) || !table1.equals(table2); int amount1 = 100; @@ -2056,19 +3472,26 @@ private void commit_NonConflictingPutsGivenForExisting_ShouldCommitBoth( int anotherFrom = NUM_TYPES * 2; int anotherTo = NUM_TYPES * 3; - populateRecords(namespace1, table1); + populateRecords(manager, namespace1, table1); if (differentTables) { - populateRecords(namespace2, table2); + populateRecords(manager, namespace2, table2); } DistributedTransaction transaction = - prepareTransfer(from, namespace1, table1, to, namespace2, table2, amount1); + prepareTransfer(manager, from, namespace1, table1, to, namespace2, table2, amount1); // Act Assert assertThatCode( () -> prepareTransfer( - anotherFrom, namespace2, table2, anotherTo, namespace1, table1, amount2) + manager, + anotherFrom, + namespace2, + table2, + anotherTo, + namespace1, + table1, + amount2) .commit()) .doesNotThrowAnyException(); @@ -2078,7 +3501,7 @@ private void commit_NonConflictingPutsGivenForExisting_ShouldCommitBoth( List gets1 = prepareGets(namespace1, table1); List gets2 = prepareGets(namespace2, table2); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional fromResult = another.get(gets1.get(from)); assertThat(fromResult).isPresent(); assertThat(getBalance(fromResult.get())).isEqualTo(INITIAL_BALANCE - amount1); @@ -2094,24 +3517,28 @@ private void commit_NonConflictingPutsGivenForExisting_ShouldCommitBoth( another.commit(); } - @Test - public void commit_NonConflictingPutsForSameTableGivenForExisting_ShouldCommitBoth() + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_NonConflictingPutsForSameTableGivenForExisting_ShouldCommitBoth(Isolation isolation) throws TransactionException { commit_NonConflictingPutsGivenForExisting_ShouldCommitBoth( - namespace1, TABLE_1, namespace1, TABLE_1); + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void commit_NonConflictingPutsForDifferentTablesGivenForExisting_ShouldCommitBoth() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_NonConflictingPutsForDifferentTablesGivenForExisting_ShouldCommitBoth( + Isolation isolation) throws TransactionException { commit_NonConflictingPutsGivenForExisting_ShouldCommitBoth( - namespace1, TABLE_1, namespace2, TABLE_2); + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } - @Test - public void putAndCommit_GetsAndPutsForSameKeyButDifferentTablesGiven_ShouldCommitBoth() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_GetsAndPutsForSameKeyButDifferentTablesGiven_ShouldCommitBoth( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); int expected = INITIAL_BALANCE; List puts1 = preparePuts(namespace1, TABLE_1); List puts2 = preparePuts(namespace2, TABLE_2); @@ -2145,7 +3572,7 @@ public void putAndCommit_GetsAndPutsForSameKeyButDifferentTablesGiven_ShouldComm // Assert List gets1 = prepareGets(namespace1, TABLE_1); List gets2 = prepareGets(namespace2, TABLE_2); - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional fromResult = another.get(gets1.get(from)); assertThat(fromResult).isPresent(); assertThat(getBalance(fromResult.get())).isEqualTo(expected); @@ -2161,75 +3588,30 @@ public void putAndCommit_GetsAndPutsForSameKeyButDifferentTablesGiven_ShouldComm another.commit(); } - @Test - public void commit_DeleteGivenWithoutRead_ShouldNotThrowAnyExceptions() - throws TransactionException { - // Arrange - Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); - - // Act Assert - transaction.delete(delete); - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void commit_DeleteGivenForNonExisting_ShouldNotThrowAnyExceptions() - throws TransactionException { - // Arrange - Get get = prepareGet(0, 0, namespace1, TABLE_1); - Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); - - // Act Assert - transaction.get(get); - transaction.delete(delete); - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void commit_DeleteGivenForExistingAfterRead_ShouldDeleteRecord() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - Get get = prepareGet(0, 0, namespace1, TABLE_1); - Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); - - // Act - Optional result = transaction.get(get); - transaction.delete(delete); - transaction.commit(); - - // Assert - assertThat(result.isPresent()).isTrue(); - DistributedTransaction another = manager.begin(); - assertThat(another.get(get).isPresent()).isFalse(); - another.commit(); - } - private void commit_ConflictingDeletesGivenForExisting_ShouldCommitOneAndAbortTheOther( - String namespace1, String table1, String namespace2, String table2) + Isolation isolation, String namespace1, String table1, String namespace2, String table2) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); boolean differentTables = !namespace1.equals(namespace2) || !table1.equals(table2); int account1 = 0; int account2 = NUM_TYPES; int account3 = NUM_TYPES * 2; - populateRecords(namespace1, table1); + populateRecords(manager, namespace1, table1); if (differentTables) { - populateRecords(namespace2, table2); + populateRecords(manager, namespace2, table2); } DistributedTransaction transaction = - prepareDeletes(account1, namespace1, table1, account2, namespace2, table2); + prepareDeletes(manager, account1, namespace1, table1, account2, namespace2, table2); // Act assertThatCode( () -> - prepareDeletes(account2, namespace2, table2, account3, namespace1, table1).commit()) + prepareDeletes(manager, account2, namespace2, table2, account3, namespace1, table1) + .commit()) .doesNotThrowAnyException(); assertThatThrownBy(transaction::commit).isInstanceOf(CommitException.class); @@ -2239,7 +3621,7 @@ private void commit_ConflictingDeletesGivenForExisting_ShouldCommitOneAndAbortTh List gets1 = prepareGets(namespace1, table1); List gets2 = differentTables ? prepareGets(namespace2, table2) : gets1; - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); Optional result = another.get(gets1.get(account1)); assertThat(result).isPresent(); assertThat(getBalance(result.get())).isEqualTo(INITIAL_BALANCE); @@ -2248,26 +3630,27 @@ private void commit_ConflictingDeletesGivenForExisting_ShouldCommitOneAndAbortTh another.commit(); } - @Test - public void - commit_ConflictingDeletesForSameTableGivenForExisting_ShouldCommitOneAndAbortTheOther() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_ConflictingDeletesForSameTableGivenForExisting_ShouldCommitOneAndAbortTheOther( + Isolation isolation) throws TransactionException { commit_ConflictingDeletesGivenForExisting_ShouldCommitOneAndAbortTheOther( - namespace1, TABLE_1, namespace1, TABLE_1); + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void - commit_ConflictingDeletesForDifferentTablesGivenForExisting_ShouldCommitOneAndAbortTheOther() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_ConflictingDeletesForDifferentTablesGivenForExisting_ShouldCommitOneAndAbortTheOther( + Isolation isolation) throws TransactionException { commit_ConflictingDeletesGivenForExisting_ShouldCommitOneAndAbortTheOther( - namespace1, TABLE_1, namespace2, TABLE_2); + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } private void commit_NonConflictingDeletesGivenForExisting_ShouldCommitBoth( - String namespace1, String table1, String namespace2, String table2) + Isolation isolation, String namespace1, String table1, String namespace2, String table2) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); boolean differentTables = !namespace1.equals(namespace2) || !table1.equals(table2); int account1 = 0; @@ -2275,18 +3658,19 @@ private void commit_NonConflictingDeletesGivenForExisting_ShouldCommitBoth( int account3 = NUM_TYPES * 2; int account4 = NUM_TYPES * 3; - populateRecords(namespace1, table1); + populateRecords(manager, namespace1, table1); if (differentTables) { - populateRecords(namespace2, table2); + populateRecords(manager, namespace2, table2); } DistributedTransaction transaction = - prepareDeletes(account1, namespace1, table1, account2, namespace2, table2); + prepareDeletes(manager, account1, namespace1, table1, account2, namespace2, table2); // Act assertThatCode( () -> - prepareDeletes(account3, namespace2, table2, account4, namespace1, table1).commit()) + prepareDeletes(manager, account3, namespace2, table2, account4, namespace1, table1) + .commit()) .doesNotThrowAnyException(); assertThatCode(transaction::commit).doesNotThrowAnyException(); @@ -2294,7 +3678,7 @@ private void commit_NonConflictingDeletesGivenForExisting_ShouldCommitBoth( // Assert List gets1 = prepareGets(namespace1, table1); List gets2 = differentTables ? prepareGets(namespace2, table2) : gets1; - DistributedTransaction another = manager.begin(); + DistributedTransaction another = manager.beginReadOnly(); assertThat(another.get(gets1.get(account1)).isPresent()).isFalse(); assertThat(another.get(gets2.get(account2)).isPresent()).isFalse(); assertThat(another.get(gets2.get(account3)).isPresent()).isFalse(); @@ -2302,24 +3686,27 @@ private void commit_NonConflictingDeletesGivenForExisting_ShouldCommitBoth( another.commit(); } - @Test - public void commit_NonConflictingDeletesForSameTableGivenForExisting_ShouldCommitBoth() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_NonConflictingDeletesForSameTableGivenForExisting_ShouldCommitBoth( + Isolation isolation) throws TransactionException { commit_NonConflictingDeletesGivenForExisting_ShouldCommitBoth( - namespace1, TABLE_1, namespace1, TABLE_1); + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void commit_NonConflictingDeletesForDifferentTablesGivenForExisting_ShouldCommitBoth() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_NonConflictingDeletesForDifferentTablesGivenForExisting_ShouldCommitBoth( + Isolation isolation) throws TransactionException { commit_NonConflictingDeletesGivenForExisting_ShouldCommitBoth( - namespace1, TABLE_1, namespace2, TABLE_2); + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } - private void commit_WriteSkewOnExistingRecordsWithSnapshot_ShouldProduceNonSerializableResult( - String namespace1, String table1, String namespace2, String table2) + private void commit_WriteSkewOnExistingRecords_ShouldBehaveCorrectly( + Isolation isolation, String namespace1, String table1, String namespace2, String table2) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); List puts = Arrays.asList( preparePut(0, 0, namespace1, table1).withValue(BALANCE, 1), @@ -2328,7 +3715,7 @@ private void commit_WriteSkewOnExistingRecordsWithSnapshot_ShouldProduceNonSeria transaction.put(puts); transaction.commit(); - // Act + // Act Assert DistributedTransaction transaction1 = manager.begin(); DistributedTransaction transaction2 = manager.begin(); @@ -2349,115 +3736,68 @@ private void commit_WriteSkewOnExistingRecordsWithSnapshot_ShouldProduceNonSeria Put put2 = preparePut(0, 1, namespace2, table2).withValue(BALANCE, current2 + 1); transaction2.put(put2); transaction1.commit(); - transaction2.commit(); - - // Assert - transaction = manager.begin(); - // the results can not be produced by executing the transactions serially - result1 = transaction.get(get1_1); - assertThat(result1).isPresent(); - assertThat(getBalance(result1.get())).isEqualTo(2); - result2 = transaction.get(get2_1); - assertThat(result2).isPresent(); - assertThat(getBalance(result2.get())).isEqualTo(2); - transaction.commit(); - } - - @Test - public void - commit_WriteSkewOnExistingRecordsInSameTableWithSnapshot_ShouldProduceNonSerializableResult() - throws TransactionException { - commit_WriteSkewOnExistingRecordsWithSnapshot_ShouldProduceNonSerializableResult( - namespace1, TABLE_1, namespace1, TABLE_1); - } - - @Test - public void - commit_WriteSkewOnExistingRecordsInDifferentTablesWithSnapshot_ShouldProduceNonSerializableResult() - throws TransactionException { - commit_WriteSkewOnExistingRecordsWithSnapshot_ShouldProduceNonSerializableResult( - namespace1, TABLE_1, namespace2, TABLE_2); - } - private void - commit_WriteSkewOnExistingRecordsWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException( - String namespace1, String table1, String namespace2, String table2) - throws TransactionException { - // Arrange - List puts = - Arrays.asList( - preparePut(0, 0, namespace1, table1).withValue(BALANCE, 1), - preparePut(0, 1, namespace2, table2).withValue(BALANCE, 1)); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - transaction.put(puts); - transaction.commit(); + if (isolation == Isolation.SERIALIZABLE) { + // In SERIALIZABLE isolation, one transaction should commit and the other should throw + // CommitConflictException - // Act - DistributedTransaction transaction1 = manager.begin(Isolation.SERIALIZABLE); - DistributedTransaction transaction2 = manager.begin(Isolation.SERIALIZABLE); + Throwable thrown = catchThrowable(transaction2::commit); + assertThat(thrown).isInstanceOf(CommitConflictException.class); - Get get1_1 = prepareGet(0, 1, namespace2, table2); - Optional result1 = transaction1.get(get1_1); - assertThat(result1).isPresent(); - int current1 = getBalance(result1.get()); - Get get1_2 = prepareGet(0, 0, namespace1, table1); - transaction1.get(get1_2); - Get get2_1 = prepareGet(0, 0, namespace1, table1); - Optional result2 = transaction2.get(get2_1); - assertThat(result2).isPresent(); - int current2 = getBalance(result2.get()); - Get get2_2 = prepareGet(0, 1, namespace2, table2); - transaction2.get(get2_2); - Put put1 = preparePut(0, 0, namespace1, table1).withValue(BALANCE, current1 + 1); - transaction1.put(put1); - Put put2 = preparePut(0, 1, namespace2, table2).withValue(BALANCE, current2 + 1); - transaction2.put(put2); - transaction1.commit(); - Throwable thrown = catchThrowable(transaction2::commit); + transaction = manager.beginReadOnly(); + result1 = transaction.get(get1_1); + assertThat(result1).isPresent(); + assertThat(getBalance(result1.get())).isEqualTo(1); + result2 = transaction.get(get2_1); + assertThat(result2).isPresent(); + assertThat(getBalance(result2.get())).isEqualTo(2); + transaction.commit(); + } else { + assert isolation == Isolation.READ_COMMITTED || isolation == Isolation.SNAPSHOT; - // Assert - transaction = manager.begin(Isolation.SERIALIZABLE); - result1 = transaction.get(get1_1); - assertThat(result1).isPresent(); - assertThat(getBalance(result1.get())).isEqualTo(1); - result2 = transaction.get(get2_1); - assertThat(result2).isPresent(); - assertThat(getBalance(result2.get())).isEqualTo(2); - transaction.commit(); + // In READ_COMMITTED or SNAPSHOT isolation, both transactions should commit successfully - assertThat(thrown).isInstanceOf(CommitConflictException.class); - } + transaction2.commit(); - @Test - public void - commit_WriteSkewOnExistingRecordsInSameTableWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException() - throws TransactionException { - commit_WriteSkewOnExistingRecordsWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException( - namespace1, TABLE_1, namespace1, TABLE_1); + transaction = manager.beginReadOnly(); + // The results can not be produced by executing the transactions serially + result1 = transaction.get(get1_1); + assertThat(result1).isPresent(); + assertThat(getBalance(result1.get())).isEqualTo(2); + result2 = transaction.get(get2_1); + assertThat(result2).isPresent(); + assertThat(getBalance(result2.get())).isEqualTo(2); + transaction.commit(); + } } - @Test - public void - commit_WriteSkewOnExistingRecordsInDifferentTablesWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException() - throws TransactionException { - commit_WriteSkewOnExistingRecordsWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException( - namespace1, TABLE_1, namespace2, TABLE_2); + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_WriteSkewOnExistingRecordsInSameTable_ShouldBehaveCorrectly(Isolation isolation) + throws TransactionException { + commit_WriteSkewOnExistingRecords_ShouldBehaveCorrectly( + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - private boolean isGroupCommitEnabled() { - return consensusCommitConfig.isCoordinatorGroupCommitEnabled(); + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_WriteSkewOnExistingRecordsInDifferentTables_ShouldBehaveCorrectly(Isolation isolation) + throws TransactionException { + commit_WriteSkewOnExistingRecords_ShouldBehaveCorrectly( + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } - private void - commit_WriteSkewOnNonExistingRecordsWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException( - String namespace1, String table1, String namespace2, String table2) - throws TransactionException { + private void commit_WriteSkewOnNonExistingRecords_ShouldBehaveCorrectly( + Isolation isolation, String namespace1, String table1, String namespace2, String table2) + throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + // no records // Act - DistributedTransaction transaction1 = manager.begin(Isolation.SERIALIZABLE); - DistributedTransaction transaction2 = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction1 = manager.begin(); + DistributedTransaction transaction2 = manager.begin(); Get get1_1 = prepareGet(0, 1, namespace2, table2); Optional result1 = transaction1.get(get1_1); Get get1_2 = prepareGet(0, 0, namespace1, table1); @@ -2478,44 +3818,74 @@ private boolean isGroupCommitEnabled() { // Assert assertThat(result1.isPresent()).isFalse(); assertThat(result2.isPresent()).isFalse(); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - result1 = transaction.get(get1_1); - assertThat(result1.isPresent()).isFalse(); - result2 = transaction.get(get2_1); - assertThat(result2.isPresent()).isTrue(); - assertThat(getBalance(result2.get())).isEqualTo(1); - transaction.commit(); - assertThat(thrown1).doesNotThrowAnyException(); - assertThat(thrown2).isInstanceOf(CommitConflictException.class); + if (isolation == Isolation.SERIALIZABLE) { + // In SERIALIZABLE isolation, one transaction should commit and the other should throw + // CommitConflictException + + DistributedTransaction transaction = manager.beginReadOnly(); + result1 = transaction.get(get1_1); + assertThat(result1.isPresent()).isFalse(); + result2 = transaction.get(get2_1); + assertThat(result2.isPresent()).isTrue(); + assertThat(getBalance(result2.get())).isEqualTo(1); + transaction.commit(); + + assertThat(thrown1).doesNotThrowAnyException(); + assertThat(thrown2).isInstanceOf(CommitConflictException.class); + } else { + assert isolation == Isolation.READ_COMMITTED || isolation == Isolation.SNAPSHOT; + + // In READ_COMMITTED or SNAPSHOT isolation, both transactions should commit successfully + + DistributedTransaction transaction = manager.beginReadOnly(); + // The results can not be produced by executing the transactions serially + result1 = transaction.get(get1_1); + assertThat(result1.isPresent()).isTrue(); + assertThat(getBalance(result1.get())).isEqualTo(1); + result2 = transaction.get(get2_1); + assertThat(result2.isPresent()).isTrue(); + assertThat(getBalance(result2.get())).isEqualTo(1); + transaction.commit(); + + assertThat(thrown1).doesNotThrowAnyException(); + assertThat(thrown2).doesNotThrowAnyException(); + } } - @Test - public void - commit_WriteSkewOnNonExistingRecordsInSameTableWithSerializable_OneShouldCommitTheOtherShouldThrowCommitException() - throws TransactionException { - commit_WriteSkewOnNonExistingRecordsWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException( - namespace1, TABLE_1, namespace1, TABLE_1); + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_WriteSkewOnNonExistingRecordsInSameTable_ShouldBehaveCorrectly(Isolation isolation) + throws TransactionException { + commit_WriteSkewOnNonExistingRecords_ShouldBehaveCorrectly( + isolation, namespace1, TABLE_1, namespace1, TABLE_1); } - @Test - public void - commit_WriteSkewOnNonExistingRecordsInDifferentTablesWithSerializable_OneShouldCommitTheOtherShouldThrowCommitException() - throws TransactionException { - commit_WriteSkewOnNonExistingRecordsWithSerializable_OneShouldCommitTheOtherShouldThrowCommitConflictException( - namespace1, TABLE_1, namespace2, TABLE_2); + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_WriteSkewOnNonExistingRecordsInDifferentTables_ShouldBehaveCorrectly( + Isolation isolation) throws TransactionException { + commit_WriteSkewOnNonExistingRecords_ShouldBehaveCorrectly( + isolation, namespace1, TABLE_1, namespace2, TABLE_2); } - @Test - public void - commit_WriteSkewWithScanOnNonExistingRecordsWithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_WriteSkewWithScanOnExistingRecords_ShouldBehaveCorrectly(Isolation isolation) + throws TransactionException { // Arrange - // no records + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + List puts = + Arrays.asList( + preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1), + preparePut(0, 1, namespace1, TABLE_1).withValue(BALANCE, 1)); + DistributedTransaction transaction = manager.begin(); + transaction.put(puts); + transaction.commit(); // Act - DistributedTransaction transaction1 = manager.begin(Isolation.SERIALIZABLE); - DistributedTransaction transaction2 = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction1 = manager.begin(); + DistributedTransaction transaction2 = manager.begin(); List results1 = transaction1.scan(prepareScan(0, 0, 1, namespace1, TABLE_1)); int count1 = results1.size(); List results2 = transaction2.scan(prepareScan(0, 0, 1, namespace1, TABLE_1)); @@ -2528,36 +3898,53 @@ private boolean isGroupCommitEnabled() { Throwable thrown2 = catchThrowable(transaction2::commit); // Assert - assertThat(results1).isEmpty(); - assertThat(results2).isEmpty(); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Optional result1 = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); - assertThat(result1.isPresent()).isTrue(); - assertThat(getBalance(result1.get())).isEqualTo(1); - Optional result2 = transaction.get(prepareGet(0, 1, namespace1, TABLE_1)); - assertThat(result2.isPresent()).isFalse(); - transaction.commit(); + if (isolation == Isolation.SERIALIZABLE) { + // In SERIALIZABLE isolation, one transaction should commit and the other should throw + // CommitConflictException + + transaction = manager.beginReadOnly(); + Optional result1 = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(result1).isPresent(); + assertThat(getBalance(result1.get())).isEqualTo(3); + Optional result2 = transaction.get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(result2).isPresent(); + assertThat(getBalance(result2.get())).isEqualTo(1); + transaction.commit(); + + assertThat(thrown1).doesNotThrowAnyException(); + assertThat(thrown2).isInstanceOf(CommitConflictException.class); + } else { + assert isolation == Isolation.READ_COMMITTED || isolation == Isolation.SNAPSHOT; + + // In READ_COMMITTED or SNAPSHOT isolation, both transactions should commit successfully + + transaction = manager.beginReadOnly(); + // The results can not be produced by executing the transactions serially + Optional result1 = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(result1).isPresent(); + assertThat(getBalance(result1.get())).isEqualTo(3); + Optional result2 = transaction.get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(result2).isPresent(); + assertThat(getBalance(result2.get())).isEqualTo(3); + transaction.commit(); - assertThat(thrown1).doesNotThrowAnyException(); - assertThat(thrown2).isInstanceOf(CommitConflictException.class); + assertThat(thrown1).doesNotThrowAnyException(); + assertThat(thrown2).doesNotThrowAnyException(); + } } - @Test - public void - commit_WriteSkewWithScanOnExistingRecordsWithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_WriteSkewWithScanOnNonExistingRecords_ShouldBehaveCorrectly(Isolation isolation) + throws TransactionException { // Arrange - List puts = - Arrays.asList( - preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1), - preparePut(0, 1, namespace1, TABLE_1).withValue(BALANCE, 1)); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - transaction.put(puts); - transaction.commit(); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + + // no records // Act - DistributedTransaction transaction1 = manager.begin(Isolation.SERIALIZABLE); - DistributedTransaction transaction2 = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction1 = manager.begin(); + DistributedTransaction transaction2 = manager.begin(); List results1 = transaction1.scan(prepareScan(0, 0, 1, namespace1, TABLE_1)); int count1 = results1.size(); List results2 = transaction2.scan(prepareScan(0, 0, 1, namespace1, TABLE_1)); @@ -2570,36 +3957,102 @@ private boolean isGroupCommitEnabled() { Throwable thrown2 = catchThrowable(transaction2::commit); // Assert - transaction = manager.begin(Isolation.SERIALIZABLE); - Optional result1 = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); - assertThat(result1).isPresent(); - assertThat(getBalance(result1.get())).isEqualTo(3); - Optional result2 = transaction.get(prepareGet(0, 1, namespace1, TABLE_1)); - assertThat(result2).isPresent(); - assertThat(getBalance(result2.get())).isEqualTo(1); - transaction.commit(); + assertThat(results1).isEmpty(); + assertThat(results2).isEmpty(); + + if (isolation == Isolation.SERIALIZABLE) { + // In SERIALIZABLE isolation, one transaction should commit and the other should throw + // CommitConflictException + + DistributedTransaction transaction = manager.beginReadOnly(); + Optional result1 = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(result1.isPresent()).isTrue(); + assertThat(getBalance(result1.get())).isEqualTo(1); + Optional result2 = transaction.get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(result2.isPresent()).isFalse(); + transaction.commit(); + + assertThat(thrown1).doesNotThrowAnyException(); + assertThat(thrown2).isInstanceOf(CommitConflictException.class); + } else { + assert isolation == Isolation.READ_COMMITTED || isolation == Isolation.SNAPSHOT; + + // In READ_COMMITTED or SNAPSHOT isolation, both transactions should commit successfully - assertThat(thrown1).doesNotThrowAnyException(); - assertThat(thrown2).isInstanceOf(CommitConflictException.class); + DistributedTransaction transaction = manager.beginReadOnly(); + // The results can not be produced by executing the transactions serially + Optional result1 = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); + assertThat(result1.isPresent()).isTrue(); + assertThat(getBalance(result1.get())).isEqualTo(1); + Optional result2 = transaction.get(prepareGet(0, 1, namespace1, TABLE_1)); + assertThat(result2.isPresent()).isTrue(); + assertThat(getBalance(result2.get())).isEqualTo(1); + transaction.commit(); + + assertThat(thrown1).doesNotThrowAnyException(); + assertThat(thrown2).doesNotThrowAnyException(); + } } - @Test - public void scanAndCommit_MultipleScansGivenInTransactionWithSerializable_ShouldCommitProperly() + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_DeleteGivenWithoutRead_ShouldNotThrowAnyExceptions(Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); + DistributedTransaction transaction = manager.begin(); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - transaction.scan(prepareScan(0, namespace1, TABLE_1)); - transaction.scan(prepareScan(1, namespace1, TABLE_1)); + transaction.delete(delete); assertThatCode(transaction::commit).doesNotThrowAnyException(); } - @Test - public void putAndCommit_DeleteGivenInBetweenTransactions_ShouldProduceSerializableResults() + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_DeleteGivenForNonExisting_ShouldNotThrowAnyExceptions(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + Get get = prepareGet(0, 0, namespace1, TABLE_1); + Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); + DistributedTransaction transaction = manager.begin(); + + // Act Assert + transaction.get(get); + transaction.delete(delete); + assertThatCode(transaction::commit).doesNotThrowAnyException(); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void commit_DeleteGivenForExistingAfterRead_ShouldDeleteRecord(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + Get get = prepareGet(0, 0, namespace1, TABLE_1); + Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); + DistributedTransaction transaction = manager.begin(); + + // Act + Optional result = transaction.get(get); + transaction.delete(delete); + transaction.commit(); + + // Assert + assertThat(result.isPresent()).isTrue(); + DistributedTransaction another = manager.beginReadOnly(); + assertThat(another.get(get).isPresent()).isFalse(); + another.commit(); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void putAndCommit_DeleteGivenInBetweenTransactions_ShouldProduceSerializableResults( + Isolation isolation) throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 2)); transaction.commit(); @@ -2632,17 +4085,19 @@ public void putAndCommit_DeleteGivenInBetweenTransactions_ShouldProduceSerializa // Assert assertThat(thrown).isInstanceOf(CommitConflictException.class); - transaction = manager.begin(); + transaction = manager.beginReadOnly(); Optional result = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); transaction.commit(); assertThat(result).isPresent(); assertThat(getBalance(result.get())).isEqualTo(1); } - @Test - public void deleteAndCommit_DeleteGivenInBetweenTransactions_ShouldProduceSerializableResults() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void deleteAndCommit_DeleteGivenInBetweenTransactions_ShouldProduceSerializableResults( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 2)); transaction.commit(); @@ -2671,17 +4126,19 @@ public void deleteAndCommit_DeleteGivenInBetweenTransactions_ShouldProduceSerial // Assert assertThat(thrown).isInstanceOf(CommitConflictException.class); - transaction = manager.begin(); + transaction = manager.beginReadOnly(); Optional result = transaction.get(prepareGet(0, 0, namespace1, TABLE_1)); transaction.commit(); assertThat(result).isPresent(); assertThat(getBalance(result.get())).isEqualTo(1); } - @Test - public void get_PutThenGetWithoutConjunctionReturnEmptyFromStorage_ShouldReturnResult() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void get_PutThenGetWithoutConjunctionReturnEmptyFromStorage_ShouldReturnResult( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); // Act @@ -2695,11 +4152,12 @@ public void get_PutThenGetWithoutConjunctionReturnEmptyFromStorage_ShouldReturnR assertThat(getBalance(result.get())).isEqualTo(1); } - @Test - public void - get_PutThenGetWithConjunctionReturnEmptyFromStorageAndMatchedWithPut_ShouldReturnResult() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void get_PutThenGetWithConjunctionReturnEmptyFromStorageAndMatchedWithPut_ShouldReturnResult( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); // Act @@ -2716,11 +4174,12 @@ public void get_PutThenGetWithoutConjunctionReturnEmptyFromStorage_ShouldReturnR assertThat(getBalance(result.get())).isEqualTo(1); } - @Test - public void - get_PutThenGetWithConjunctionReturnEmptyFromStorageAndUnmatchedWithPut_ShouldReturnEmpty() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void get_PutThenGetWithConjunctionReturnEmptyFromStorageAndUnmatchedWithPut_ShouldReturnEmpty( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); // Act @@ -2736,12 +4195,13 @@ public void get_PutThenGetWithoutConjunctionReturnEmptyFromStorage_ShouldReturnR assertThat(result).isNotPresent(); } - @Test - public void - get_PutThenGetWithConjunctionReturnResultFromStorageAndMatchedWithPut_ShouldReturnResult() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void get_PutThenGetWithConjunctionReturnResultFromStorageAndMatchedWithPut_ShouldReturnResult( + Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); Put put = preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1); Get get = @@ -2759,12 +4219,13 @@ public void get_PutThenGetWithoutConjunctionReturnEmptyFromStorage_ShouldReturnR assertThat(getBalance(result.get())).isEqualTo(1); } - @Test - public void - get_PutThenGetWithConjunctionReturnResultFromStorageButUnmatchedWithPut_ShouldReturnEmpty() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void get_PutThenGetWithConjunctionReturnResultFromStorageButUnmatchedWithPut_ShouldReturnEmpty( + Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); Put put = preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1); Get get = @@ -2782,9 +4243,11 @@ public void get_PutThenGetWithoutConjunctionReturnEmptyFromStorage_ShouldReturnR assertThat(result).isNotPresent(); } - @Test - public void get_DeleteCalledBefore_ShouldReturnEmpty() throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void get_DeleteCalledBefore_ShouldReturnEmpty(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); transaction.commit(); @@ -2802,9 +4265,11 @@ public void get_DeleteCalledBefore_ShouldReturnEmpty() throws TransactionExcepti assertThat(resultAfter.isPresent()).isFalse(); } - @Test - public void delete_PutCalledBefore_ShouldDelete() throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void delete_PutCalledBefore_ShouldDelete(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); transaction.commit(); @@ -2818,17 +4283,19 @@ public void delete_PutCalledBefore_ShouldDelete() throws TransactionException { assertThatCode(transaction1::commit).doesNotThrowAnyException(); // Assert - DistributedTransaction transaction2 = manager.begin(); + DistributedTransaction transaction2 = manager.beginReadOnly(); Optional resultAfter = transaction2.get(get); transaction2.commit(); assertThat(resultBefore.isPresent()).isTrue(); assertThat(resultAfter.isPresent()).isFalse(); } - @Test - public void put_DeleteCalledBefore_ShouldThrowIllegalArgumentException() + @ParameterizedTest + @EnumSource(Isolation.class) + void put_DeleteCalledBefore_ShouldThrowIllegalArgumentException(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); transaction.commit(); @@ -2847,10 +4314,12 @@ public void put_DeleteCalledBefore_ShouldThrowIllegalArgumentException() assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void scan_OverlappingPutGivenBefore_ShouldThrowIllegalArgumentException() + @ParameterizedTest + @EnumSource(Isolation.class) + void scan_OverlappingPutGivenBefore_ShouldThrowIllegalArgumentException(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); @@ -2863,9 +4332,12 @@ public void scan_OverlappingPutGivenBefore_ShouldThrowIllegalArgumentException() assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void scan_NonOverlappingPutGivenBefore_ShouldScan() throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void scan_NonOverlappingPutGivenBefore_ShouldScan(Isolation isolation) + throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); @@ -2878,11 +4350,12 @@ public void scan_NonOverlappingPutGivenBefore_ShouldScan() throws TransactionExc assertThat(thrown).doesNotThrowAnyException(); } - @Test - public void - scan_PutWithOverlappedClusteringKeyAndNonOverlappedConjunctionsGivenBefore_ShouldScan() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void scan_PutWithOverlappedClusteringKeyAndNonOverlappedConjunctionsGivenBefore_ShouldScan( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 1, namespace1, TABLE_1).withValue(BALANCE, 1)); Scan scan = @@ -2902,12 +4375,13 @@ public void scan_NonOverlappingPutGivenBefore_ShouldScan() throws TransactionExc assertThat(thrown).doesNotThrowAnyException(); } - @Test - public void - scan_NonOverlappingPutGivenButOverlappingPutExists_ShouldThrowIllegalArgumentException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void scan_NonOverlappingPutGivenButOverlappingPutExists_ShouldThrowIllegalArgumentException( + Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 1, namespace1, TABLE_1).withValue(BALANCE, 9999)); Scan scan = @@ -2923,10 +4397,12 @@ public void scan_NonOverlappingPutGivenBefore_ShouldScan() throws TransactionExc assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArgumentException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArgumentException( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put( preparePut(0, 0, namespace1, TABLE_1) @@ -2946,11 +4422,12 @@ public void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArg assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void - scanWithIndex_PutWithOverlappedIndexKeyAndNonOverlappedConjunctionsGivenBefore_ShouldScan() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void scanWithIndex_PutWithOverlappedIndexKeyAndNonOverlappedConjunctionsGivenBefore_ShouldScan( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put( Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)) @@ -2970,12 +4447,14 @@ public void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArg assertThat(thrown).doesNotThrowAnyException(); } - @Test - public void - scanWithIndex_OverlappingPutWithNonIndexedColumnGivenBefore_ShouldThrowIllegalArgumentException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void + scanWithIndex_OverlappingPutWithNonIndexedColumnGivenBefore_ShouldThrowIllegalArgumentException( + Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); transaction.put( Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)) @@ -2991,12 +4470,14 @@ public void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArg assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void - scanWithIndex_NonOverlappingPutWithIndexedColumnGivenBefore_ShouldThrowIllegalArgumentException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void + scanWithIndex_NonOverlappingPutWithIndexedColumnGivenBefore_ShouldThrowIllegalArgumentException( + Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); transaction.put( Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)).intValue(BALANCE, 999).build()); @@ -3010,12 +4491,13 @@ public void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArg assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void - scanWithIndex_OverlappingPutWithIndexedColumnGivenBefore_ShouldThrowIllegalArgumentException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void scanWithIndex_OverlappingPutWithIndexedColumnGivenBefore_ShouldThrowIllegalArgumentException( + Isolation isolation) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); DistributedTransaction transaction = manager.begin(); transaction.put( Put.newBuilder(preparePut(0, 0, namespace1, TABLE_1)).intValue(BALANCE, 999).build()); @@ -3029,11 +4511,13 @@ public void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArg assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void - scanWithIndex_OverlappingPutWithIndexedColumnAndConjunctionsGivenBefore_ShouldThrowIllegalArgumentException() - throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void + scanWithIndex_OverlappingPutWithIndexedColumnAndConjunctionsGivenBefore_ShouldThrowIllegalArgumentException( + Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put( preparePut(0, 0, namespace1, TABLE_1) @@ -3053,10 +4537,12 @@ public void scan_OverlappingPutWithConjunctionsGivenBefore_ShouldThrowIllegalArg assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void scan_DeleteGivenBefore_ShouldThrowIllegalArgumentException() + @ParameterizedTest + @EnumSource(Isolation.class) + void scan_DeleteGivenBefore_ShouldThrowIllegalArgumentException(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); transaction.put(preparePut(0, 1, namespace1, TABLE_1).withValue(BALANCE, 1)); @@ -3070,34 +4556,12 @@ public void scan_DeleteGivenBefore_ShouldThrowIllegalArgumentException() transaction1.rollback(); } - @Test - public void begin_CorrectTransactionIdGiven_ShouldNotThrowAnyExceptions() { - // Arrange - String transactionId = ANY_ID_1; - - // Act Assert - assertThatCode( - () -> { - DistributedTransaction transaction = manager.begin(transactionId); - transaction.commit(); - }) - .doesNotThrowAnyException(); - } - - @Test - public void begin_EmptyTransactionIdGiven_ShouldThrowIllegalArgumentException() { - // Arrange - String transactionId = ""; - - // Act Assert - assertThatThrownBy(() -> manager.begin(transactionId)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void scanAll_DeleteGivenBefore_ShouldThrowIllegalArgumentException() + @ParameterizedTest + @EnumSource(Isolation.class) + void scanAll_DeleteGivenBefore_ShouldThrowIllegalArgumentException(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withIntValue(BALANCE, 1)); transaction.put(preparePut(0, 1, namespace1, TABLE_1).withIntValue(BALANCE, 1)); @@ -3112,9 +4576,12 @@ public void scanAll_DeleteGivenBefore_ShouldThrowIllegalArgumentException() transaction1.rollback(); } - @Test - public void scanAll_NonOverlappingPutGivenBefore_ShouldScanAll() throws TransactionException { + @ParameterizedTest + @EnumSource(Isolation.class) + void scanAll_NonOverlappingPutGivenBefore_ShouldScanAll(Isolation isolation) + throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withIntValue(BALANCE, 1)); @@ -3127,10 +4594,12 @@ public void scanAll_NonOverlappingPutGivenBefore_ShouldScanAll() throws Transact assertThat(thrown).doesNotThrowAnyException(); } - @Test - public void scanAll_OverlappingPutGivenBefore_ShouldThrowIllegalArgumentException() + @ParameterizedTest + @EnumSource(Isolation.class) + void scanAll_OverlappingPutGivenBefore_ShouldThrowIllegalArgumentException(Isolation isolation) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withIntValue(BALANCE, 1)); @@ -3143,12 +4612,14 @@ public void scanAll_OverlappingPutGivenBefore_ShouldThrowIllegalArgumentExceptio assertThat(thrown).isInstanceOf(IllegalArgumentException.class); } - @Test - public void scanAll_ScanAllGivenForCommittedRecord_ShouldReturnRecord() - throws TransactionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void scanAll_ScanAllGivenForCommittedRecord_ShouldReturnRecord( + Isolation isolation, boolean readOnly) throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); ScanAll scanAll = prepareScanAll(namespace1, TABLE_1).withLimit(1); // Act @@ -3162,14 +4633,17 @@ public void scanAll_ScanAllGivenForCommittedRecord_ShouldReturnRecord() .isEqualTo(TransactionState.COMMITTED); } - @Test - public void scanAll_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws TransactionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void scanAll_ScanAllGivenForNonExisting_ShouldReturnEmpty(Isolation isolation, boolean readOnly) + throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction putTransaction = manager.begin(); putTransaction.put(preparePut(0, 0, namespace1, TABLE_1)); putTransaction.commit(); - DistributedTransaction transaction = manager.begin(); + DistributedTransaction transaction = begin(manager, readOnly); ScanAll scanAll = prepareScanAll(namespace2, TABLE_2); // Act @@ -3181,324 +4655,65 @@ public void scanAll_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws Transa } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scanAll_ScanAllGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - scanAll, commitType, isolation, false); - } + @MethodSource("isolationAndReadOnlyMode") + void get_GetWithMatchedConjunctionsGivenForCommittedRecord_ShouldReturnRecord( + Isolation isolation, boolean readOnly) throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Get get = + Get.newBuilder(prepareGet(0, 0, namespace1, TABLE_1)) + .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) + .and(column(SOME_COLUMN).isNullText()) + .build(); - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanAllGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateAborted_ShouldRollback( - scanAll, commitType, isolation, true); - } + // Act + Optional result = transaction.get(get); + transaction.commit(); - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scanAll_ScanAllGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - scanAll, commitType, isolation, false); + // Assert + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(result.get())).isEqualTo(INITIAL_BALANCE); + assertThat(result.get().getText(SOME_COLUMN)).isNull(); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanAllGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateCommitted_ShouldRollforward( - scanAll, commitType, isolation, true); - } + @MethodSource("isolationAndReadOnlyMode") + void get_GetWithUnmatchedConjunctionsGivenForCommittedRecord_ShouldReturnEmpty( + Isolation isolation, boolean readOnly) throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + DistributedTransaction transaction = begin(manager, readOnly); + Get get = + Get.newBuilder(prepareGet(0, 0, namespace1, TABLE_1)) + .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) + .and(column(SOME_COLUMN).isEqualToText("aaa")) + .build(); - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - scanAll_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scanAll, commitType, isolation, false); + // Act + Optional result = transaction.get(get); + transaction.commit(); + + // Assert + assertThat(result.isPresent()).isFalse(); } @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scanAll, commitType, isolation, true); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - scanAll_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scanAll, commitType, isolation, false); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanAllGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForDeletedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scanAll, commitType, isolation, true); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scanAll_ScanAllGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) - throws TransactionException, ExecutionException, CoordinatorException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - scanAll, commitType, isolation, false); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanAllGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - CommitType commitType, Isolation isolation) - throws TransactionException, ExecutionException, CoordinatorException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateAborted_ShouldRollback( - scanAll, commitType, isolation, true); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void scanAll_ScanAllGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - scanAll, commitType, isolation, false); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void getScanner_ScanAllGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateCommitted_ShouldRollforward( - scanAll, commitType, isolation, true); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - scanAll_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scanAll, commitType, isolation, false); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndExpired_ShouldAbortTransaction( - scanAll, commitType, isolation, true); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - scanAll_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scanAll, commitType, isolation, false); - } - - @ParameterizedTest - @MethodSource("commitTypeAndIsolation") - public void - getScanner_ScanAllGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - CommitType commitType, Isolation isolation) - throws ExecutionException, CoordinatorException, TransactionException { - // ScalarDB 3 doesn't have `coordinator.state.tx_child_ids` column when the group commit feature - // is disabled. Skip this test in this case. - if (!isGroupCommitEnabled() && commitType != CommitType.NORMAL_COMMIT) { - return; - } - ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); - selection_SelectionGivenForPreparedWhenCoordinatorStateNotExistAndNotExpired_ShouldNotAbortTransaction( - scanAll, commitType, isolation, true); - } - - @Test - public void get_GetWithMatchedConjunctionsGivenForCommittedRecord_ShouldReturnRecord() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Get get = - Get.newBuilder(prepareGet(1, 1, namespace1, TABLE_1)) - .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) - .and(column(SOME_COLUMN).isNullText()) - .build(); - - // Act - Optional result = transaction.get(get); - transaction.commit(); - - // Assert - assertThat(result.isPresent()).isTrue(); - assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(1); - assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(result.get())).isEqualTo(INITIAL_BALANCE); - assertThat(result.get().getText(SOME_COLUMN)).isNull(); - } - - @Test - public void get_GetWithUnmatchedConjunctionsGivenForCommittedRecord_ShouldReturnEmpty() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Get get = - Get.newBuilder(prepareGet(1, 1, namespace1, TABLE_1)) - .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) - .and(column(SOME_COLUMN).isEqualToText("aaa")) - .build(); - - // Act - Optional result = transaction.get(get); - transaction.commit(); - - // Assert - assertThat(result.isPresent()).isFalse(); - } - - @Test - public void scan_CalledTwice_ShouldReturnFromSnapshotInSecondTime() - throws TransactionException, ExecutionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(); - Scan scan = prepareScan(0, 0, 0, namespace1, TABLE_1); - - // Act - List result1 = transaction.scan(scan); - List result2 = transaction.scan(scan); - transaction.commit(); - - // Assert - verify(storage).scan(any(Scan.class)); - assertThat(result1).isEqualTo(result2); - } - - @Test - public void scan_CalledTwiceWithSameConditionsAndUpdateForHappenedInBetween_ShouldReadRepeatably() - throws TransactionException { + @MethodSource("isolationAndReadOnlyMode") + void scan_CalledTwiceWithSameConditionsAndUpdateForHappenedInBetween_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); transaction.commit(); - DistributedTransaction transaction1 = manager.begin(); + // Act Assert + DistributedTransaction transaction1 = begin(manager, readOnly); Scan scan = Scan.newBuilder() .namespace(namespace1) @@ -3509,31 +4724,57 @@ public void scan_CalledTwiceWithSameConditionsAndUpdateForHappenedInBetween_Shou .build(); List result1 = transaction1.scan(scan); - DistributedTransaction transaction2 = manager.begin(); - transaction2.get(prepareGet(0, 0, namespace1, TABLE_1)); - transaction2.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 0)); - transaction2.commit(); + // The record is updated by another transaction + manager.update( + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 0) + .build()); - // Act List result2 = transaction1.scan(scan); - transaction1.commit(); - // Assert - assertThat(result1.size()).isEqualTo(1); - assertThat(result2.size()).isEqualTo(1); - assertThat(result1.get(0)).isEqualTo(result2.get(0)); + if (isolation == Isolation.READ_COMMITTED) { + // In READ_COMMITTED isolation + + transaction1.commit(); + + // The first scan should return the record + assertThat(result1.size()).isEqualTo(1); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(1); + + // The second scan should return empty as the record was updated + assertThat(result2).isEmpty(); + } else if (isolation == Isolation.SNAPSHOT) { + // In SNAPSHOT isolation + + transaction1.commit(); + + // Both scans should return the same result + assertThat(result1.size()).isEqualTo(1); + assertThat(result2.size()).isEqualTo(1); + assertThat(result1.get(0)).isEqualTo(result2.get(0)); + } else { + assert isolation == Isolation.SERIALIZABLE; + + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction1::commit).isInstanceOf(CommitConflictException.class); + } } - @Test - public void - scan_CalledTwiceWithDifferentConditionsAndUpdateHappenedInBetween_ShouldNotReadRepeatably() - throws TransactionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void scan_CalledTwiceWithDifferentConditionsAndUpdateHappenedInBetween_ShouldBehaveCorrectly( + Isolation isolation, boolean readOnly) throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); DistributedTransaction transaction = manager.begin(); transaction.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 1)); transaction.commit(); - DistributedTransaction transaction1 = manager.begin(); + DistributedTransaction transaction1 = begin(manager, readOnly); Scan scan1 = Scan.newBuilder() .namespace(namespace1) @@ -3550,185 +4791,67 @@ public void scan_CalledTwiceWithSameConditionsAndUpdateForHappenedInBetween_Shou .start(Key.ofInt(ACCOUNT_TYPE, 0)) .where(column(BALANCE).isGreaterThanInt(1)) .build(); + + // Act Assert List result1 = transaction1.scan(scan1); - DistributedTransaction transaction2 = manager.begin(); - transaction2.get(prepareGet(0, 0, namespace1, TABLE_1)); - transaction2.put(preparePut(0, 0, namespace1, TABLE_1).withValue(BALANCE, 2)); - transaction2.commit(); + // The record is updated by another transaction + manager.update( + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 2) + .build()); - // Act List result2 = transaction1.scan(scan2); - transaction1.commit(); - - // Assert - assertThat(result1.size()).isEqualTo(1); - assertThat(result2.size()).isEqualTo(1); - assertThat(result1.get(0)).isNotEqualTo(result2.get(0)); - assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(1); - assertThat(result2.get(0).getInt(BALANCE)).isEqualTo(2); - } - - @Test - @EnabledIf("isGroupCommitEnabled") - void put_WhenTheOtherTransactionsIsDelayed_ShouldBeCommittedWithoutBlocked() throws Exception { - // Arrange - // Act - DistributedTransaction slowTxn = manager.begin(); - DistributedTransaction fastTxn = manager.begin(); - fastTxn.put(preparePut(0, 0, namespace1, TABLE_1)); + if (isolation == Isolation.SERIALIZABLE) { + // In SERIALIZABLE isolation, an anti-dependency should be detected + assertThatThrownBy(transaction1::commit).isInstanceOf(CommitConflictException.class); + } else { + assert isolation == Isolation.READ_COMMITTED || isolation == Isolation.SNAPSHOT; - assertTimeout(Duration.ofSeconds(10), fastTxn::commit); + // In READ_COMMITTED or SNAPSHOT isolation - slowTxn.put(preparePut(1, 0, namespace1, TABLE_1)); - slowTxn.commit(); + transaction1.commit(); - // Assert - DistributedTransaction validationTxn = manager.begin(); - assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isPresent(); - assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isPresent(); - validationTxn.commit(); + // The first scan should return the record + assertThat(result1.size()).isEqualTo(1); + assertThat(result1.get(0).getInt(BALANCE)).isEqualTo(1); - assertThat(coordinator.getState(slowTxn.getId()).get().getState()) - .isEqualTo(TransactionState.COMMITTED); - assertThat(coordinator.getState(fastTxn.getId()).get().getState()) - .isEqualTo(TransactionState.COMMITTED); + // The second scan should return the updated record + assertThat(result2.size()).isEqualTo(1); + assertThat(result2.get(0).getInt(BALANCE)).isEqualTo(2); + } } @Test - @EnabledIf("isGroupCommitEnabled") - void put_WhenTheOtherTransactionsFails_ShouldBeCommittedWithoutBlocked() throws Exception { + void scan_RecordUpdatedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() + throws TransactionException { // Arrange - doThrow(PreparationConflictException.class).when(commit).prepareRecords(any()); + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); + manager.mutate( + Arrays.asList( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build(), + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, INITIAL_BALANCE) + .build())); - // Act - DistributedTransaction failingTxn = manager.begin(); - DistributedTransaction successTxn = manager.begin(); - failingTxn.put(preparePut(0, 0, namespace1, TABLE_1)); - successTxn.put(preparePut(1, 0, namespace1, TABLE_1)); - - // This transaction will be committed after the other transaction in the same group is removed. - assertTimeout( - Duration.ofSeconds(10), - () -> { - try { - failingTxn.commit(); - fail(); - } catch (CommitConflictException e) { - // Expected - } finally { - reset(commit); - } - }); - assertTimeout(Duration.ofSeconds(10), successTxn::commit); - - // Assert - DistributedTransaction validationTxn = manager.begin(); - assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isEmpty(); - assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isPresent(); - validationTxn.commit(); - - assertThat(coordinator.getState(failingTxn.getId()).get().getState()) - .isEqualTo(TransactionState.ABORTED); - assertThat(coordinator.getState(successTxn.getId()).get().getState()) - .isEqualTo(TransactionState.COMMITTED); - } - - @Test - @EnabledIf("isGroupCommitEnabled") - void put_WhenTransactionFailsDueToConflict_ShouldBeAbortedWithoutBlocked() throws Exception { - // Arrange - - // Act - DistributedTransaction failingTxn = manager.begin(Isolation.SERIALIZABLE); - DistributedTransaction successTxn = manager.begin(Isolation.SERIALIZABLE); - failingTxn.get(prepareGet(1, 0, namespace1, TABLE_1)); - failingTxn.put(preparePut(0, 0, namespace1, TABLE_1)); - successTxn.put(preparePut(1, 0, namespace1, TABLE_1)); - - // This transaction will be committed after the other transaction in the same group - // is moved to a delayed group. - assertTimeout(Duration.ofSeconds(10), successTxn::commit); - assertTimeout( - Duration.ofSeconds(10), - () -> { - try { - failingTxn.commit(); - fail(); - } catch (CommitConflictException e) { - // Expected - } - }); - - // Assert - DistributedTransaction validationTxn = manager.begin(); - assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isEmpty(); - assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isPresent(); - validationTxn.commit(); - - assertThat(coordinator.getState(failingTxn.getId()).get().getState()) - .isEqualTo(TransactionState.ABORTED); - assertThat(coordinator.getState(successTxn.getId()).get().getState()) - .isEqualTo(TransactionState.COMMITTED); - } - - @Test - @EnabledIf("isGroupCommitEnabled") - void put_WhenAllTransactionsAbort_ShouldBeAbortedProperly() throws Exception { - // Act - DistributedTransaction failingTxn1 = manager.begin(Isolation.SERIALIZABLE); - DistributedTransaction failingTxn2 = manager.begin(Isolation.SERIALIZABLE); - - doThrow(PreparationConflictException.class).when(commit).prepareRecords(any()); - - failingTxn1.put(preparePut(0, 0, namespace1, TABLE_1)); - failingTxn2.put(preparePut(1, 0, namespace1, TABLE_1)); - - try { - assertThat(catchThrowable(failingTxn1::commit)).isInstanceOf(CommitConflictException.class); - assertThat(catchThrowable(failingTxn2::commit)).isInstanceOf(CommitConflictException.class); - } finally { - reset(commit); - } - - // Assert - DistributedTransaction validationTxn = manager.begin(); - assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isEmpty(); - assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isEmpty(); - validationTxn.commit(); - - assertThat(coordinator.getState(failingTxn1.getId()).get().getState()) - .isEqualTo(TransactionState.ABORTED); - assertThat(coordinator.getState(failingTxn2.getId()).get().getState()) - .isEqualTo(TransactionState.ABORTED); - } - - @Test - public void - scan_RecordUpdatedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { - // Arrange - manager.mutate( - Arrays.asList( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); + // Act Assert + DistributedTransaction transaction = manager.begin(); + List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); @@ -3738,7 +4861,7 @@ void put_WhenAllTransactionsAbort_ShouldBeAbortedProperly() throws Exception { assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.update( Update.newBuilder() .namespace(namespace1) @@ -3753,9 +4876,10 @@ void put_WhenAllTransactionsAbort_ShouldBeAbortedProperly() throws Exception { } @Test - public void scan_RecordUpdatedByMySelf_WithSerializable_ShouldNotThrowAnyException() + void scan_RecordUpdatedByMySelf_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -3774,7 +4898,7 @@ public void scan_RecordUpdatedByMySelf_WithSerializable_ShouldNotThrowAnyExcepti .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); @@ -3798,10 +4922,11 @@ public void scan_RecordUpdatedByMySelf_WithSerializable_ShouldNotThrowAnyExcepti } @Test - public void + void scan_FirstRecordInsertedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -3820,7 +4945,7 @@ public void scan_RecordUpdatedByMySelf_WithSerializable_ShouldNotThrowAnyExcepti .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); @@ -3831,7 +4956,7 @@ public void scan_RecordUpdatedByMySelf_WithSerializable_ShouldNotThrowAnyExcepti assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(2); assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.insert( Insert.newBuilder() .namespace(namespace1) @@ -3846,9 +4971,10 @@ public void scan_RecordUpdatedByMySelf_WithSerializable_ShouldNotThrowAnyExcepti } @Test - public void scan_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyException() + void scan_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -3867,7 +4993,7 @@ public void scan_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyE .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); @@ -3891,10 +5017,11 @@ public void scan_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyE } @Test - public void + void scan_LastRecordInsertedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -3913,7 +5040,7 @@ public void scan_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyE .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); @@ -3924,7 +5051,7 @@ public void scan_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyE assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.insert( Insert.newBuilder() .namespace(namespace1) @@ -3939,9 +5066,10 @@ public void scan_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyE } @Test - public void scan_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyException() + void scan_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -3960,7 +5088,7 @@ public void scan_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyEx .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); @@ -3984,10 +5112,11 @@ public void scan_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyEx } @Test - public void + void scan_FirstRecordDeletedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4006,7 +5135,7 @@ public void scan_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyEx .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); @@ -4017,7 +5146,7 @@ public void scan_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyEx assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.delete( Delete.newBuilder() .namespace(namespace1) @@ -4031,9 +5160,10 @@ public void scan_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyEx } @Test - public void scan_FirstRecordDeletedByMySelf_WithSerializable_ShouldNotThrowAnyException() + void scan_FirstRecordDeletedByMySelf_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4052,7 +5182,7 @@ public void scan_FirstRecordDeletedByMySelf_WithSerializable_ShouldNotThrowAnyEx .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(prepareScan(0, namespace1, TABLE_1)); assertThat(results).hasSize(2); @@ -4075,9 +5205,10 @@ public void scan_FirstRecordDeletedByMySelf_WithSerializable_ShouldNotThrowAnyEx } @Test - public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException() + void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4096,7 +5227,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4115,10 +5246,11 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( } @Test - public void + void scan_ScanWithLimitGiven_RecordInsertedByAnotherTransaction_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4137,7 +5269,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4155,7 +5287,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.insert( Insert.newBuilder() .namespace(namespace1) @@ -4170,10 +5302,11 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( } @Test - public void + void scan_ScanWithLimitGiven_FirstRecordInsertedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4192,7 +5325,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4210,7 +5343,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(2); assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.insert( Insert.newBuilder() .namespace(namespace1) @@ -4229,6 +5362,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( scan_ScanWithLimitGiven_FirstRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4247,7 +5381,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4277,10 +5411,11 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( } @Test - public void + void scan_ScanWithLimitGiven_LastRecordInsertedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4299,7 +5434,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4316,7 +5451,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.insert( Insert.newBuilder() .namespace(namespace1) @@ -4331,10 +5466,11 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( } @Test - public void + void scan_ScanWithLimitGiven_LastRecordInsertedByMySelf_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4353,7 +5489,7 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4383,9 +5519,9 @@ public void scan_ScanWithLimitGiven_WithSerializable_ShouldNotThrowAnyException( } @Test - public void scan_ScanAllGiven_WithSerializable_ShouldNotThrowAnyException() - throws TransactionException { + void scan_ScanAllGiven_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4425,7 +5561,7 @@ public void scan_ScanAllGiven_WithSerializable_ShouldNotThrowAnyException() .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan(Scan.newBuilder().namespace(namespace1).table(TABLE_1).all().build()); @@ -4443,9 +5579,10 @@ public void scan_ScanAllGiven_WithSerializable_ShouldNotThrowAnyException() } @Test - public void scan_ScanAllWithLimitGiven_WithSerializable_ShouldNotThrowAnyException() + void scan_ScanAllWithLimitGiven_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4485,7 +5622,7 @@ public void scan_ScanAllWithLimitGiven_WithSerializable_ShouldNotThrowAnyExcepti .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder().namespace(namespace1).table(TABLE_1).all().limit(3).build()); @@ -4504,9 +5641,10 @@ public void scan_ScanAllWithLimitGiven_WithSerializable_ShouldNotThrowAnyExcepti } @Test - public void scan_ScanWithIndexGiven_WithSerializable_ShouldNotThrowAnyException() + void scan_ScanWithIndexGiven_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4546,7 +5684,7 @@ public void scan_ScanWithIndexGiven_WithSerializable_ShouldNotThrowAnyException( .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4569,9 +5707,10 @@ public void scan_ScanWithIndexGiven_WithSerializable_ShouldNotThrowAnyException( } @Test - public void scan_ScanWithIndexWithLimitGiven_WithSerializable_ShouldNotThrowAnyException() + void scan_ScanWithIndexWithLimitGiven_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4611,7 +5750,7 @@ public void scan_ScanWithIndexWithLimitGiven_WithSerializable_ShouldNotThrowAnyE .build())); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); List results = transaction.scan( Scan.newBuilder() @@ -4635,9 +5774,10 @@ public void scan_ScanWithIndexWithLimitGiven_WithSerializable_ShouldNotThrowAnyE } @Test - public void get_GetWithIndexGiven_WithSerializable_ShouldNotThrowAnyException() + void get_GetWithIndexGiven_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.insert( Insert.newBuilder() .namespace(namespace1) @@ -4648,7 +5788,7 @@ public void get_GetWithIndexGiven_WithSerializable_ShouldNotThrowAnyException() .build()); // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); Optional actual = transaction.get( Get.newBuilder() @@ -4666,8 +5806,9 @@ public void get_GetWithIndexGiven_WithSerializable_ShouldNotThrowAnyException() } @Test - public void getScanner_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { + void getScanner_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -4685,780 +5826,84 @@ public void getScanner_WithSerializable_ShouldNotThrowAnyException() throws Tran .intValue(BALANCE, INITIAL_BALANCE) .build(), Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - - // Act Assert - TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); - Optional result1 = scanner.one(); - assertThat(result1).isNotEmpty(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - Optional result2 = scanner.one(); - assertThat(result2).isNotEmpty(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - scanner.close(); - - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void - getScanner_FirstInsertedRecordByAnotherTransaction_WithSerializable_ShouldNotThrowCommitConflictException() - throws TransactionException { - // Arrange - manager.mutate( - Arrays.asList( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - - // Act Assert - TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); - Optional result1 = scanner.one(); - assertThat(result1).isNotEmpty(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - Optional result2 = scanner.one(); - assertThat(result2).isNotEmpty(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - scanner.close(); - - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - another.commit(); - - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); - } - - @Test - public void - getScanner_RecordInsertedByAnotherTransaction_WithSerializable_ShouldNotThrowAnyException() - throws TransactionException { - // Arrange - manager.mutate( - Arrays.asList( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - - // Act Assert - TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); - Optional result1 = scanner.one(); - assertThat(result1).isNotEmpty(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - Optional result2 = scanner.one(); - assertThat(result2).isNotEmpty(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - scanner.close(); - - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - another.commit(); - - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void - getScanner_RecordUpdatedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { - // Arrange - manager.mutate( - Arrays.asList( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - - // Act Assert - TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); - Optional result1 = scanner.one(); - assertThat(result1).isNotEmpty(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - Optional result2 = scanner.one(); - assertThat(result2).isNotEmpty(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - scanner.close(); - - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 0) - .build()); - another.commit(); - - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); - } - - @Test - public void - get_GetWithIndexGiven_NoRecordsInIndexRange_WithSerializable_ShouldNotThrowAnyException() - throws TransactionException { - // Arrange - - // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Optional actual = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) - .build()); - - assertThat(actual).isEmpty(); - - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void - get_GetWithIndexGiven_RecordInsertedIntoIndexRangeByMySelf_WithSerializable_ShouldNotThrowAnyException() - throws TransactionException { - // Arrange - manager.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - - // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Optional actual = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) - .build()); - - assertThat(actual).isPresent(); - assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(actual.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - transaction.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void - get_GetWithIndexGiven_RecordInsertedIntoIndexRangeByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { - // Arrange - manager.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - - // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Optional actual = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) - .build()); - - assertThat(actual).isPresent(); - assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(actual.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - another.commit(); - - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); - } - - @Test - public void - get_GetWithIndexGiven_NoRecordsInIndexRange_RecordInsertedIntoIndexRangeByMySelf_WithSerializable_ShouldNotThrowAnyException() - throws TransactionException { - // Arrange - - // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Optional actual = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) - .build()); - - assertThat(actual).isEmpty(); - - transaction.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void - get_GetWithIndexGiven_NoRecordsInIndexRange_RecordInsertedIntoIndexRangeByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { - // Arrange - - // Act Assert - DistributedTransaction transaction = manager.begin(Isolation.SERIALIZABLE); - Optional actual = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) - .build()); - - assertThat(actual).isEmpty(); - - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - another.commit(); - - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); - } - - @Test - public void getAndUpdate_GetWithIndexGiven_ShouldUpdate() throws TransactionException { - // Arrange - manager.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - - // Act Assert - DistributedTransaction transaction = manager.begin(); - Optional actual = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) - .build()); - - assertThat(actual).isPresent(); - assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(actual.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - transaction.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) - .build()); - - transaction.commit(); - - transaction = manager.begin(); - actual = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .build()); - transaction.commit(); - - assertThat(actual).isPresent(); - assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(actual.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(actual.get().getInt(BALANCE)).isEqualTo(1); - } - - @Test - public void scanAndUpdate_ScanWithIndexGiven_ShouldUpdate() throws TransactionException { - // Arrange - manager.mutate( - Arrays.asList( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - // Act Assert - DistributedTransaction transaction = manager.begin(); - List actualResults = - transaction.scan( - Scan.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) - .build()); - - assertThat(actualResults).hasSize(2); - Set expectedTypes = Sets.newHashSet(0, 1); - for (Result result : actualResults) { - assertThat(result.getInt(ACCOUNT_ID)).isEqualTo(0); - expectedTypes.remove(result.getInt(ACCOUNT_TYPE)); - assertThat(result.getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - } - assertThat(expectedTypes).isEmpty(); - - transaction.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) - .build()); - transaction.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, 2) - .build()); - - transaction.commit(); - - transaction = manager.begin(); - Optional actual1 = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .build()); - Optional actual2 = - transaction.get( - Get.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .build()); - transaction.commit(); - - assertThat(actual1).isPresent(); - assertThat(actual1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(actual1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(actual1.get().getInt(BALANCE)).isEqualTo(1); - - assertThat(actual2).isPresent(); - assertThat(actual2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(actual2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(actual2.get().getInt(BALANCE)).isEqualTo(2); - } - - @Test - public void get_GetGivenForCommittedRecord_InReadOnlyMode_WithSerializable_ShouldReturnRecord() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); - Get get = prepareGet(0, 0, namespace1, TABLE_1); - - // Act - Optional result = transaction.get(get); - transaction.commit(); - - // Assert - assertThat(result.isPresent()).isTrue(); - assertThat(((TransactionResult) ((FilteredResult) result.get()).getOriginalResult()).getState()) - .isEqualTo(TransactionState.COMMITTED); - } - - @Test - public void - get_GetGivenForCommittedRecord_InReadOnlyMode_WhenRecordUpdatedByAnotherTransaction_ShouldNotThrowAnyException() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(); - Get get = prepareGet(0, 0, namespace1, TABLE_1); - - // Act Assert - Optional result = transaction.get(get); - assertThat(result.isPresent()).isTrue(); - assertThat(getBalance(result.get())).isEqualTo(INITIAL_BALANCE); - - DistributedTransaction another = manager.begin(); - another.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) - .build()); - another.commit(); - - assertThatCode(transaction::commit).doesNotThrowAnyException(); - } - - @Test - public void - get_GetGivenForCommittedRecord_InReadOnlyMode_WhenRecordUpdatedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); - Get get = prepareGet(0, 0, namespace1, TABLE_1); - - // Act Assert - Optional result = transaction.get(get); - assertThat(result.isPresent()).isTrue(); - assertThat(getBalance(result.get())).isEqualTo(INITIAL_BALANCE); - - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) - .build()); - another.commit(); - - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); - } - - @Test - public void scan_ScanGivenForCommittedRecord_InReadOnlyMode_WithSerializable_ShouldReturnRecord() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); - Scan scan = prepareScan(0, namespace1, TABLE_1); - - // Act - List results = transaction.scan(scan); - transaction.commit(); - - // Assert - assertThat(results.size()).isEqualTo(4); - assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(results.get(0))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(results.get(1))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(getBalance(results.get(2))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); - assertThat(getBalance(results.get(3))).isEqualTo(INITIAL_BALANCE); - } - - @Test - public void - scan_ScanGivenForCommittedRecord_InReadOnlyMode_WhenRecordUpdatedByAnotherTransaction_ShouldNotThrowAnyException() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(); - Scan scan = prepareScan(0, namespace1, TABLE_1); - - // Act Assert - List results = transaction.scan(scan); - - assertThat(results.size()).isEqualTo(4); - assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(results.get(0))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(results.get(1))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(getBalance(results.get(2))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); - assertThat(getBalance(results.get(3))).isEqualTo(INITIAL_BALANCE); - - DistributedTransaction another = manager.begin(); - another.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) - .build()); - another.commit(); - - transaction.commit(); - } - - @Test - public void - scan_ScanGivenForCommittedRecord_InReadOnlyMode_WhenRecordInsertedByAnotherTransaction_ShouldNotThrowAnyException() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(); - Scan scan = prepareScan(0, namespace1, TABLE_1); - - // Act Assert - List results = transaction.scan(scan); - - assertThat(results.size()).isEqualTo(4); - assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(results.get(0))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(results.get(1))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(getBalance(results.get(2))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); - assertThat(getBalance(results.get(3))).isEqualTo(INITIAL_BALANCE); - - DistributedTransaction another = manager.begin(); - another.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 5)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - another.commit(); - - transaction.commit(); - } + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) + .intValue(BALANCE, INITIAL_BALANCE) + .build())); - @Test - public void - scan_ScanGivenForCommittedRecord_InReadOnlyMode_WhenRecordUpdatedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { - // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); Scan scan = prepareScan(0, namespace1, TABLE_1); + DistributedTransaction transaction = manager.begin(); // Act Assert - List results = transaction.scan(scan); - - assertThat(results.size()).isEqualTo(4); - assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(results.get(0))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(results.get(1))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(getBalance(results.get(2))).isEqualTo(INITIAL_BALANCE); + TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); + Optional result1 = scanner.one(); + assertThat(result1).isNotEmpty(); + assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - assertThat(results.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); - assertThat(getBalance(results.get(3))).isEqualTo(INITIAL_BALANCE); + Optional result2 = scanner.one(); + assertThat(result2).isNotEmpty(); + assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.update( - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) - .build()); - another.commit(); + scanner.close(); - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + assertThatCode(transaction::commit).doesNotThrowAnyException(); } @Test - public void - scan_ScanGivenForCommittedRecord_InReadOnlyMode_WhenRecordInsertedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() + void + getScanner_FirstInsertedRecordByAnotherTransaction_WithSerializable_ShouldNotThrowCommitConflictException() throws TransactionException { // Arrange - populateRecords(namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); + manager.mutate( + Arrays.asList( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, INITIAL_BALANCE) + .build(), + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) + .intValue(BALANCE, INITIAL_BALANCE) + .build())); + Scan scan = prepareScan(0, namespace1, TABLE_1); + DistributedTransaction transaction = manager.begin(); // Act Assert - List results = transaction.scan(scan); - - assertThat(results.size()).isEqualTo(4); - assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(results.get(0))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(results.get(1))).isEqualTo(INITIAL_BALANCE); + TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); + Optional result1 = scanner.one(); + assertThat(result1).isNotEmpty(); + assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(getBalance(results.get(2))).isEqualTo(INITIAL_BALANCE); + Optional result2 = scanner.one(); + assertThat(result2).isNotEmpty(); + assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - assertThat(results.get(3).getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(results.get(3).getInt(ACCOUNT_TYPE)).isEqualTo(3); - assertThat(getBalance(results.get(3))).isEqualTo(INITIAL_BALANCE); + scanner.close(); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); + DistributedTransaction another = manager.begin(); another.insert( Insert.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 5)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) .intValue(BALANCE, INITIAL_BALANCE) .build()); another.commit(); @@ -5467,9 +5912,10 @@ public void scan_ScanGivenForCommittedRecord_InReadOnlyMode_WithSerializable_Sho } @Test - public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyException() + void getScanner_RecordInsertedByAnotherTransaction_WithSerializable_ShouldNotThrowAnyException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -5485,17 +5931,10 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) - .intValue(BALANCE, INITIAL_BALANCE) .build())); Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); + DistributedTransaction transaction = manager.begin(); // Act Assert TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); @@ -5513,14 +5952,26 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio scanner.close(); + DistributedTransaction another = manager.begin(); + another.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + another.commit(); + assertThatCode(transaction::commit).doesNotThrowAnyException(); } @Test - public void - getScanner_InReadOnlyMode_WhenRecordUpdatedByAnotherTransaction_ShouldNotThrowAnyException() + void + getScanner_RecordUpdatedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -5536,17 +5987,10 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) - .intValue(BALANCE, INITIAL_BALANCE) .build())); Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(); + DistributedTransaction transaction = manager.begin(); // Act Assert TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); @@ -5562,62 +6006,178 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - scanner.close(); + scanner.close(); + + DistributedTransaction another = manager.begin(); + another.update( + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 0) + .build()); + another.commit(); + + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } + + @Test + void get_GetWithIndexGiven_NoRecordsInIndexRange_WithSerializable_ShouldNotThrowAnyException() + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); + + // Act Assert + DistributedTransaction transaction = manager.begin(); + Optional actual = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) + .build()); + + assertThat(actual).isEmpty(); + + assertThatCode(transaction::commit).doesNotThrowAnyException(); + } + + @Test + void + get_GetWithIndexGiven_RecordInsertedIntoIndexRangeByMySelf_WithSerializable_ShouldNotThrowAnyException() + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); + manager.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + + // Act Assert + DistributedTransaction transaction = manager.begin(); + Optional actual = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) + .build()); + + assertThat(actual).isPresent(); + assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(actual.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + transaction.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + + assertThatCode(transaction::commit).doesNotThrowAnyException(); + } + + @Test + void + get_GetWithIndexGiven_RecordInsertedIntoIndexRangeByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); + manager.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + + // Act Assert + DistributedTransaction transaction = manager.begin(); + Optional actual = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) + .build()); + + assertThat(actual).isPresent(); + assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(actual.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + + DistributedTransaction another = manager.begin(); + another.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + another.commit(); + + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + } + + @Test + void + get_GetWithIndexGiven_NoRecordsInIndexRange_RecordInsertedIntoIndexRangeByMySelf_WithSerializable_ShouldNotThrowAnyException() + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); + + // Act Assert + DistributedTransaction transaction = manager.begin(); + Optional actual = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) + .build()); + + assertThat(actual).isEmpty(); - DistributedTransaction another = manager.begin(); - another.update( - Update.newBuilder() + transaction.insert( + Insert.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) + .intValue(BALANCE, INITIAL_BALANCE) .build()); - another.commit(); assertThatCode(transaction::commit).doesNotThrowAnyException(); } @Test - public void - getScanner_InReadOnlyMode_WhenRecordInsertedByAnotherTransaction_ShouldNotThrowAnyException() + void + get_GetWithIndexGiven_NoRecordsInIndexRange_RecordInsertedIntoIndexRangeByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() throws TransactionException { // Arrange - manager.mutate( - Arrays.asList( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(); + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); // Act Assert - TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); - Optional result1 = scanner.one(); - assertThat(result1).isNotEmpty(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - Optional result2 = scanner.one(); - assertThat(result2).isNotEmpty(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + DistributedTransaction transaction = manager.begin(); + Optional actual = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) + .build()); - scanner.close(); + assertThat(actual).isEmpty(); DistributedTransaction another = manager.begin(); another.insert( @@ -5630,59 +6190,41 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build()); another.commit(); - assertThatCode(transaction::commit).doesNotThrowAnyException(); + assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); } - @Test - public void - getScanner_InReadOnlyMode_WhenRecordUpdatedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { + @Disabled("Fix later") + @ParameterizedTest + @EnumSource(Isolation.class) + void getAndUpdate_GetWithIndexGiven_ShouldUpdate(Isolation isolation) + throws TransactionException { // Arrange - manager.mutate( - Arrays.asList( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(), - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) - .intValue(BALANCE, INITIAL_BALANCE) - .build())); - - Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + manager.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); // Act Assert - TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); - Optional result1 = scanner.one(); - assertThat(result1).isNotEmpty(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - Optional result2 = scanner.one(); - assertThat(result2).isNotEmpty(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + DistributedTransaction transaction = manager.begin(); + Optional result = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) + .build()); - scanner.close(); + assertThat(result).isPresent(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(result.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.update( + transaction.update( Update.newBuilder() .namespace(namespace1) .table(TABLE_1) @@ -5690,71 +6232,124 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) .intValue(BALANCE, 1) .build()); - another.commit(); - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + transaction.commit(); + + Optional actual = + manager.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build()); + assertThat(actual).isPresent(); + assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(actual.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(actual.get().getInt(BALANCE)).isEqualTo(1); } - @Test - public void - getScanner_InReadOnlyMode_WhenRecordInsertedByAnotherTransaction_WithSerializable_ShouldThrowCommitConflictException() - throws TransactionException { + @Disabled("Fix later") + @ParameterizedTest + @EnumSource(Isolation.class) + void scanAndUpdate_ScanWithIndexGiven_ShouldUpdate(Isolation isolation) + throws TransactionException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.mutate( Arrays.asList( Insert.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) .intValue(BALANCE, INITIAL_BALANCE) .build(), Insert.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 2)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) .intValue(BALANCE, INITIAL_BALANCE) .build())); - Scan scan = prepareScan(0, namespace1, TABLE_1); - DistributedTransaction transaction = manager.beginReadOnly(Isolation.SERIALIZABLE); - // Act Assert - TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); - Optional result1 = scanner.one(); - assertThat(result1).isNotEmpty(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(result1.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); - - Optional result2 = scanner.one(); - assertThat(result2).isNotEmpty(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(result2.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + DistributedTransaction transaction = manager.begin(); + List results = + transaction.scan( + Scan.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .indexKey(Key.ofInt(BALANCE, INITIAL_BALANCE)) + .build()); - scanner.close(); + assertThat(results).hasSize(2); + Set expectedTypes = Sets.newHashSet(0, 1); + for (Result result : results) { + assertThat(result.getInt(ACCOUNT_ID)).isEqualTo(0); + expectedTypes.remove(result.getInt(ACCOUNT_TYPE)); + assertThat(result.getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); + } + assertThat(expectedTypes).isEmpty(); - DistributedTransaction another = manager.begin(Isolation.SERIALIZABLE); - another.insert( - Insert.newBuilder() + transaction.update( + Update.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) + .intValue(BALANCE, 1) + .build()); + transaction.update( + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .intValue(BALANCE, 2) .build()); - another.commit(); - assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); + transaction.commit(); + + transaction = manager.beginReadOnly(); + Optional actual1 = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .build()); + Optional actual2 = + transaction.get( + Get.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 1)) + .build()); + transaction.commit(); + + assertThat(actual1).isPresent(); + assertThat(actual1.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(actual1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(actual1.get().getInt(BALANCE)).isEqualTo(1); + + assertThat(actual2).isPresent(); + assertThat(actual2.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(actual2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(actual2.get().getInt(BALANCE)).isEqualTo(2); } - @Test - public void - get_WithConjunction_ForPreparedRecordWhoseBeforeImageMatchesConjunction_ShouldReturnRecordAfterLazyRecovery() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + get_WithConjunction_ForPreparedRecordWhoseBeforeImageMatchesConjunction_ShouldReturnRecordAfterLazyRecovery( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.insert( Insert.newBuilder() .namespace(namespace1) @@ -5791,8 +6386,9 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio originalStorage.mutate(prepareMutationComposer.get()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); Optional actual = - manager.get( + transaction.get( Get.newBuilder() .namespace(namespace1) .table(TABLE_1) @@ -5800,6 +6396,7 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) .build()); + transaction.commit(); assertThat(actual).isPresent(); assertThat(actual.get().getInt(ACCOUNT_ID)).isEqualTo(0); @@ -5807,11 +6404,15 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(actual.get().getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } - @Test - public void - get_WithConjunction_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + get_WithConjunction_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.insert( Insert.newBuilder() .namespace(namespace1) @@ -5858,6 +6459,7 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); Optional actual = manager.get( Get.newBuilder() @@ -5867,15 +6469,20 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) .build()); + transaction.commit(); assertThat(actual).isNotPresent(); } - @Test - public void - scan_WithConjunction_ForPreparedRecordWhoseBeforeImageMatchesConjunction_ShouldReturnRecordAfterLazyRecovery() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + scan_WithConjunction_ForPreparedRecordWhoseBeforeImageMatchesConjunction_ShouldReturnRecordAfterLazyRecovery( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -5920,14 +6527,16 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio originalStorage.mutate(prepareMutationComposer.get()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); List results = - manager.scan( + transaction.scan( Scan.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) .build()); + transaction.commit(); assertThat(results).hasSize(2); assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); @@ -5938,11 +6547,15 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } - @Test - public void - scan_WithConjunction_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + scan_WithConjunction_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -5997,14 +6610,16 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); List results = - manager.scan( + transaction.scan( Scan.newBuilder() .namespace(namespace1) .table(TABLE_1) .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) .build()); + transaction.commit(); assertThat(results).hasSize(1); assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); @@ -6012,11 +6627,15 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(results.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } - @Test - public void - scan_WithConjunctionAndLimit_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + scan_WithConjunctionAndLimit_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -6085,8 +6704,9 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); List results = - manager.scan( + transaction.scan( Scan.newBuilder() .namespace(namespace1) .table(TABLE_1) @@ -6094,6 +6714,7 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .where(column(BALANCE).isEqualToInt(INITIAL_BALANCE)) .limit(2) .build()); + transaction.commit(); assertThat(results).hasSize(2); assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); @@ -6104,11 +6725,15 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } - @Test - public void - getScanner_WithConjunction_ForPreparedRecordWhoseBeforeImageMatchesConjunction_ShouldReturnRecordAfterLazyRecovery() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + getScanner_WithConjunction_ForPreparedRecordWhoseBeforeImageMatchesConjunction_ShouldReturnRecordAfterLazyRecovery( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -6153,9 +6778,10 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio originalStorage.mutate(prepareMutationComposer.get()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); List results; - try (TransactionManagerCrudOperable.Scanner scanner = - manager.getScanner( + try (TransactionCrudOperable.Scanner scanner = + transaction.getScanner( Scan.newBuilder() .namespace(namespace1) .table(TABLE_1) @@ -6164,6 +6790,7 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build())) { results = scanner.all(); } + transaction.commit(); assertThat(results).hasSize(2); assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); @@ -6174,11 +6801,15 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } - @Test - public void - getScanner_WithConjunction_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + getScanner_WithConjunction_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -6233,9 +6864,10 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); List results; - try (TransactionManagerCrudOperable.Scanner scanner = - manager.getScanner( + try (TransactionCrudOperable.Scanner scanner = + transaction.getScanner( Scan.newBuilder() .namespace(namespace1) .table(TABLE_1) @@ -6244,6 +6876,7 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build())) { results = scanner.all(); } + transaction.commit(); assertThat(results).hasSize(1); assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); @@ -6251,11 +6884,15 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(results.get(0).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } - @Test - public void - getScanner_WithConjunctionAndLimit_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord() - throws UnknownTransactionStatusException, CrudException, ExecutionException { + @ParameterizedTest + @MethodSource("isolationAndReadOnlyMode") + void + getScanner_WithConjunctionAndLimit_ForCommittedRecordWhoseBeforeImageMatchesConjunction_ShouldNotReturnRecord( + Isolation isolation, boolean readOnly) + throws UnknownTransactionStatusException, CrudException, ExecutionException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.mutate( Arrays.asList( Insert.newBuilder() @@ -6324,9 +6961,10 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build()); // Act Assert + DistributedTransaction transaction = begin(manager, readOnly); List results; - try (TransactionManagerCrudOperable.Scanner scanner = - manager.getScanner( + try (TransactionCrudOperable.Scanner scanner = + transaction.getScanner( Scan.newBuilder() .namespace(namespace1) .table(TABLE_1) @@ -6336,6 +6974,7 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .build())) { results = scanner.all(); } + transaction.commit(); assertThat(results).hasSize(2); assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(0); @@ -6346,11 +6985,15 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(results.get(1).getInt(BALANCE)).isEqualTo(INITIAL_BALANCE); } - @Test - public void - commit_ConflictingExternalUpdate_DifferentGetButSameRecordReturned_ShouldThrowCommitConflictExceptionAndPreserveExternalChanges() - throws UnknownTransactionStatusException, CrudException, RollbackException { + @ParameterizedTest + @EnumSource(Isolation.class) + void + commit_ConflictingExternalUpdate_DifferentGetButSameRecordReturned_ShouldThrowShouldBehaveCorrectly( + Isolation isolation) + throws UnknownTransactionStatusException, CrudException, RollbackException, + CommitException { // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); manager.insert( Insert.newBuilder() .namespace(namespace1) @@ -6412,10 +7055,11 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio assertThat(result).isNotPresent(); + // CommitConflictException should be thrown because the record was updated by another + // transaction assertThatThrownBy(transaction::commit).isInstanceOf(CommitConflictException.class); transaction.rollback(); - // Assert result = manager.get( Get.newBuilder() @@ -6425,381 +7069,563 @@ public void getScanner_InReadOnlyMode_WithSerializable_ShouldNotThrowAnyExceptio .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) .build()); + // The record should still exist with the updated balance by the other transaction assertThat(result).isPresent(); assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); assertThat(result.get().getInt(BALANCE)).isEqualTo(200); } - @Test - public void manager_get_GetGivenForCommittedRecord_WithSerializable_ShouldReturnRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - Get get = prepareGet(0, 0, namespace1, TABLE_1); + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_get_GetGivenForCommittedRecord_ShouldReturnRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + Get get = prepareGet(0, 0, namespace1, TABLE_1); + + // Act + Optional result = manager.get(get); + + // Assert + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); + assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(result.get())).isEqualTo(INITIAL_BALANCE); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_scan_ScanGivenForCommittedRecord_ShouldReturnRecords(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1); + + // Act + List results = manager.scan(scan); + + // Assert + assertThat(results.size()).isEqualTo(3); + assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(results.get(0))).isEqualTo(INITIAL_BALANCE); + + assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(getBalance(results.get(1))).isEqualTo(INITIAL_BALANCE); + + assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(getBalance(results.get(2))).isEqualTo(INITIAL_BALANCE); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_getScanner_ScanGivenForCommittedRecord_ShouldReturnRecords(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecords(manager, namespace1, TABLE_1); + Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1); + + // Act Assert + TransactionManagerCrudOperable.Scanner scanner = manager.getScanner(scan); + + Optional result1 = scanner.one(); + assertThat(result1).isPresent(); + assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); + assertThat(getBalance(result1.get())).isEqualTo(INITIAL_BALANCE); + + Optional result2 = scanner.one(); + assertThat(result2).isPresent(); + assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); + assertThat(getBalance(result2.get())).isEqualTo(INITIAL_BALANCE); + + Optional result3 = scanner.one(); + assertThat(result3).isPresent(); + assertThat(result3.get().getInt(ACCOUNT_ID)).isEqualTo(1); + assertThat(result3.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); + assertThat(getBalance(result3.get())).isEqualTo(INITIAL_BALANCE); + + assertThat(scanner.one()).isNotPresent(); + + scanner.close(); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_put_PutGivenForNonExisting_ShouldCreateRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + int expected = INITIAL_BALANCE; + Put put = + Put.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, expected) + .build(); + + // Act + manager.put(put); + + // Assert + Get get = prepareGet(0, 0, namespace1, TABLE_1); + Optional result = manager.get(get); + + assertThat(result.isPresent()).isTrue(); + assertThat(getBalance(result.get())).isEqualTo(expected); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_put_PutGivenForExisting_ShouldUpdateRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + + // Act + int expected = INITIAL_BALANCE + 100; + Put put = + Put.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, expected) + .enableImplicitPreRead() + .build(); + manager.put(put); + + // Assert + Optional actual = manager.get(prepareGet(0, 0, namespace1, TABLE_1)); + + assertThat(actual.isPresent()).isTrue(); + assertThat(getBalance(actual.get())).isEqualTo(expected); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_insert_InsertGivenForNonExisting_ShouldCreateRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + int expected = INITIAL_BALANCE; + Insert insert = + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, expected) + .build(); + + // Act + manager.insert(insert); + + // Assert + Get get = prepareGet(0, 0, namespace1, TABLE_1); + Optional result = manager.get(get); + + assertThat(result.isPresent()).isTrue(); + assertThat(getBalance(result.get())).isEqualTo(expected); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_insert_InsertGivenForExisting_ShouldThrowCrudConflictException(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + + // Act Assert + int expected = INITIAL_BALANCE + 100; + Insert insert = + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, expected) + .build(); + + assertThatThrownBy(() -> manager.insert(insert)).isInstanceOf(CrudConflictException.class); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_upsert_UpsertGivenForNonExisting_ShouldCreateRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + int expected = INITIAL_BALANCE; + Upsert upsert = + Upsert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, expected) + .build(); + + // Act + manager.upsert(upsert); + + // Assert + Get get = prepareGet(0, 0, namespace1, TABLE_1); + Optional result = manager.get(get); + + assertThat(result.isPresent()).isTrue(); + assertThat(getBalance(result.get())).isEqualTo(expected); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_upsert_UpsertGivenForExisting_ShouldUpdateRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + + // Act + int expected = INITIAL_BALANCE + 100; + Upsert upsert = + Upsert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, expected) + .build(); + manager.upsert(upsert); + + // Assert + Optional actual = manager.get(prepareGet(0, 0, namespace1, TABLE_1)); + + assertThat(actual.isPresent()).isTrue(); + assertThat(getBalance(actual.get())).isEqualTo(expected); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_update_UpdateGivenForNonExisting_ShouldDoNothing(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + Update update = + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build(); + + // Act + assertThatCode(() -> manager.update(update)).doesNotThrowAnyException(); + + // Assert + Optional actual = manager.get(prepareGet(0, 0, namespace1, TABLE_1)); + + assertThat(actual).isEmpty(); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_update_UpdateGivenForExisting_ShouldUpdateRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + + // Act + int expected = INITIAL_BALANCE + 100; + Update update = + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, expected) + .build(); + manager.update(update); + + // Assert + Optional actual = manager.get(prepareGet(0, 0, namespace1, TABLE_1)); + + assertThat(actual.isPresent()).isTrue(); + assertThat(getBalance(actual.get())).isEqualTo(expected); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_delete_DeleteGivenForExisting_ShouldDeleteRecord(Isolation isolation) + throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + populateRecord(manager, namespace1, TABLE_1); + Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); + + // Act + manager.delete(delete); + + // Assert + Optional result = manager.get(prepareGet(0, 0, namespace1, TABLE_1)); + + assertThat(result.isPresent()).isFalse(); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + void manager_mutate_ShouldMutateRecords(Isolation isolation) throws TransactionException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + manager.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + manager.insert( + Insert.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + + Update update = + Update.newBuilder() + .namespace(namespace1) + .table(TABLE_1) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, 1) + .build(); + Delete delete = prepareDelete(1, 0, namespace1, TABLE_1); + + // Act + manager.mutate(Arrays.asList(update, delete)); + + // Assert + Optional result1 = manager.get(prepareGet(0, 0, namespace1, TABLE_1)); + Optional result2 = manager.get(prepareGet(1, 0, namespace1, TABLE_1)); + + assertThat(result1.isPresent()).isTrue(); + assertThat(getBalance(result1.get())).isEqualTo(1); + + assertThat(result2.isPresent()).isFalse(); + } + + @ParameterizedTest + @EnumSource(Isolation.class) + public void putAndCommit_SinglePartitionMutationsGiven_ShouldAccessStorageOnceForPrepareAndCommit( + Isolation isolation) throws TransactionException, ExecutionException, CoordinatorException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + IntValue balance = new IntValue(BALANCE, INITIAL_BALANCE); + List puts = preparePuts(namespace1, TABLE_1); + puts.get(0).withValue(balance); + puts.get(1).withValue(balance); + DistributedTransaction transaction = manager.begin(); - // Act - Optional result = managerWithSerializable.get(get); + // Act + transaction.put(puts.get(0)); + transaction.put(puts.get(1)); + transaction.commit(); - // Assert - assertThat(result.isPresent()).isTrue(); - assertThat(result.get().getInt(ACCOUNT_ID)).isEqualTo(0); - assertThat(result.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(result.get())).isEqualTo(INITIAL_BALANCE); + // Assert + // one for prepare, one for commit + verify(storage, times(2)).mutate(anyList()); + if (isGroupCommitEnabled()) { + verify(coordinator) + .putStateForGroupCommit(anyString(), anyList(), any(TransactionState.class), anyLong()); + return; } + verify(coordinator).putState(any(Coordinator.State.class)); } - @Test - public void manager_scan_ScanGivenForCommittedRecord_WithSerializable_ShouldReturnRecords() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1); - - // Act - List results = managerWithSerializable.scan(scan); - - // Assert - assertThat(results.size()).isEqualTo(3); - assertThat(results.get(0).getInt(ACCOUNT_ID)).isEqualTo(1); - assertThat(results.get(0).getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(results.get(0))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(1).getInt(ACCOUNT_ID)).isEqualTo(1); - assertThat(results.get(1).getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(results.get(1))).isEqualTo(INITIAL_BALANCE); - - assertThat(results.get(2).getInt(ACCOUNT_ID)).isEqualTo(1); - assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(getBalance(results.get(2))).isEqualTo(INITIAL_BALANCE); + @ParameterizedTest + @EnumSource(Isolation.class) + public void putAndCommit_TwoPartitionsMutationsGiven_ShouldAccessStorageTwiceForPrepareAndCommit( + Isolation isolation) throws TransactionException, ExecutionException, CoordinatorException { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(isolation); + IntValue balance = new IntValue(BALANCE, INITIAL_BALANCE); + List puts = preparePuts(namespace1, TABLE_1); + puts.get(0).withValue(balance); + puts.get(NUM_TYPES).withValue(balance); // next account + DistributedTransaction transaction = manager.begin(); + + // Act + transaction.put(puts.get(0)); + transaction.put(puts.get(NUM_TYPES)); + transaction.commit(); + + // Assert + // twice for prepare, twice for commit + verify(storage, times(4)).mutate(anyList()); + if (isGroupCommitEnabled()) { + verify(coordinator) + .putStateForGroupCommit(anyString(), anyList(), any(TransactionState.class), anyLong()); + return; } + verify(coordinator).putState(any(Coordinator.State.class)); } @Test - public void manager_getScanner_ScanGivenForCommittedRecord_WithSerializable_ShouldReturnRecords() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1); - - // Act Assert - TransactionManagerCrudOperable.Scanner scanner = managerWithSerializable.getScanner(scan); + @EnabledIf("isGroupCommitEnabled") + void put_WhenTheOtherTransactionsIsDelayed_ShouldBeCommittedWithoutBlocked() throws Exception { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SNAPSHOT); - Optional result1 = scanner.one(); - assertThat(result1).isPresent(); - assertThat(result1.get().getInt(ACCOUNT_ID)).isEqualTo(1); - assertThat(result1.get().getInt(ACCOUNT_TYPE)).isEqualTo(0); - assertThat(getBalance(result1.get())).isEqualTo(INITIAL_BALANCE); + // Act + DistributedTransaction slowTxn = manager.begin(); + DistributedTransaction fastTxn = manager.begin(); + fastTxn.put(preparePut(0, 0, namespace1, TABLE_1)); - Optional result2 = scanner.one(); - assertThat(result2).isPresent(); - assertThat(result2.get().getInt(ACCOUNT_ID)).isEqualTo(1); - assertThat(result2.get().getInt(ACCOUNT_TYPE)).isEqualTo(1); - assertThat(getBalance(result2.get())).isEqualTo(INITIAL_BALANCE); + assertTimeout(Duration.ofSeconds(10), fastTxn::commit); - Optional result3 = scanner.one(); - assertThat(result3).isPresent(); - assertThat(result3.get().getInt(ACCOUNT_ID)).isEqualTo(1); - assertThat(result3.get().getInt(ACCOUNT_TYPE)).isEqualTo(2); - assertThat(getBalance(result3.get())).isEqualTo(INITIAL_BALANCE); + slowTxn.put(preparePut(1, 0, namespace1, TABLE_1)); + slowTxn.commit(); - assertThat(scanner.one()).isNotPresent(); + // Assert + DistributedTransaction validationTxn = manager.beginReadOnly(); + assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isPresent(); + assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isPresent(); + validationTxn.commit(); - scanner.close(); - } + assertThat(coordinator.getState(slowTxn.getId()).get().getState()) + .isEqualTo(TransactionState.COMMITTED); + assertThat(coordinator.getState(fastTxn.getId()).get().getState()) + .isEqualTo(TransactionState.COMMITTED); } @Test - public void manager_put_PutGivenForNonExisting_WithSerializable_ShouldCreateRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - int expected = INITIAL_BALANCE; - Put put = - Put.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expected) - .build(); - - // Act - managerWithSerializable.put(put); - - // Assert - Get get = prepareGet(0, 0, namespace1, TABLE_1); - Optional result = managerWithSerializable.get(get); - - assertThat(result.isPresent()).isTrue(); - assertThat(getBalance(result.get())).isEqualTo(expected); - } - } + @EnabledIf("isGroupCommitEnabled") + void put_WhenTheOtherTransactionsFails_ShouldBeCommittedWithoutBlocked() throws Exception { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SNAPSHOT); + doThrow(PreparationConflictException.class).when(commit).prepareRecords(any()); - @Test - public void manager_put_PutGivenForExisting_WithSerializable_ShouldUpdateRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - - // Act - int expected = INITIAL_BALANCE + 100; - Put put = - Put.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expected) - .enableImplicitPreRead() - .build(); - managerWithSerializable.put(put); - - // Assert - Optional actual = managerWithSerializable.get(prepareGet(0, 0, namespace1, TABLE_1)); - - assertThat(actual.isPresent()).isTrue(); - assertThat(getBalance(actual.get())).isEqualTo(expected); - } - } + // Act + DistributedTransaction failingTxn = manager.begin(); + DistributedTransaction successTxn = manager.begin(); + failingTxn.put(preparePut(0, 0, namespace1, TABLE_1)); + successTxn.put(preparePut(1, 0, namespace1, TABLE_1)); - @Test - public void manager_insert_InsertGivenForNonExisting_WithSerializable_ShouldCreateRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - int expected = INITIAL_BALANCE; - Insert insert = - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expected) - .build(); - - // Act - managerWithSerializable.insert(insert); - - // Assert - Get get = prepareGet(0, 0, namespace1, TABLE_1); - Optional result = managerWithSerializable.get(get); + // This transaction will be committed after the other transaction in the same group is removed. + assertTimeout( + Duration.ofSeconds(10), + () -> { + try { + failingTxn.commit(); + fail(); + } catch (CommitConflictException e) { + // Expected + } finally { + reset(commit); + } + }); + assertTimeout(Duration.ofSeconds(10), successTxn::commit); - assertThat(result.isPresent()).isTrue(); - assertThat(getBalance(result.get())).isEqualTo(expected); - } - } + // Assert + DistributedTransaction validationTxn = manager.beginReadOnly(); + assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isEmpty(); + assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isPresent(); + validationTxn.commit(); - @Test - public void - manager_insert_InsertGivenForExisting_WithSerializable_ShouldThrowCrudConflictException() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - - // Act Assert - int expected = INITIAL_BALANCE + 100; - Insert insert = - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expected) - .build(); - - assertThatThrownBy(() -> managerWithSerializable.insert(insert)) - .isInstanceOf(CrudConflictException.class); - } + assertThat(coordinator.getState(failingTxn.getId()).get().getState()) + .isEqualTo(TransactionState.ABORTED); + assertThat(coordinator.getState(successTxn.getId()).get().getState()) + .isEqualTo(TransactionState.COMMITTED); } @Test - public void manager_upsert_UpsertGivenForNonExisting_WithSerializable_ShouldCreateRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - int expected = INITIAL_BALANCE; - Upsert upsert = - Upsert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expected) - .build(); - - // Act - managerWithSerializable.upsert(upsert); - - // Assert - Get get = prepareGet(0, 0, namespace1, TABLE_1); - Optional result = managerWithSerializable.get(get); + @EnabledIf("isGroupCommitEnabled") + void put_WhenTransactionFailsDueToConflict_ShouldBeAbortedWithoutBlocked() throws Exception { + // Arrange + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); - assertThat(result.isPresent()).isTrue(); - assertThat(getBalance(result.get())).isEqualTo(expected); - } - } + // Act + DistributedTransaction failingTxn = manager.begin(); + DistributedTransaction successTxn = manager.begin(); + failingTxn.get(prepareGet(1, 0, namespace1, TABLE_1)); + failingTxn.put(preparePut(0, 0, namespace1, TABLE_1)); + successTxn.put(preparePut(1, 0, namespace1, TABLE_1)); - @Test - public void manager_upsert_UpsertGivenForExisting_WithSerializable_ShouldUpdateRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - - // Act - int expected = INITIAL_BALANCE + 100; - Upsert upsert = - Upsert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expected) - .build(); - managerWithSerializable.upsert(upsert); - - // Assert - Optional actual = managerWithSerializable.get(prepareGet(0, 0, namespace1, TABLE_1)); - - assertThat(actual.isPresent()).isTrue(); - assertThat(getBalance(actual.get())).isEqualTo(expected); - } - } + // This transaction will be committed after the other transaction in the same group + // is moved to a delayed group. + assertTimeout(Duration.ofSeconds(10), successTxn::commit); + assertTimeout( + Duration.ofSeconds(10), + () -> { + try { + failingTxn.commit(); + fail(); + } catch (CommitConflictException e) { + // Expected + } + }); - @Test - public void manager_update_UpdateGivenForNonExisting_WithSerializable_ShouldDoNothing() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - Update update = - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build(); - - // Act - assertThatCode(() -> managerWithSerializable.update(update)).doesNotThrowAnyException(); - - // Assert - Optional actual = managerWithSerializable.get(prepareGet(0, 0, namespace1, TABLE_1)); - - assertThat(actual).isEmpty(); - } - } + // Assert + DistributedTransaction validationTxn = manager.beginReadOnly(); + assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isEmpty(); + assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isPresent(); + validationTxn.commit(); - @Test - public void manager_update_UpdateGivenForExisting_WithSerializable_ShouldUpdateRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - - // Act - int expected = INITIAL_BALANCE + 100; - Update update = - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, expected) - .build(); - managerWithSerializable.update(update); - - // Assert - Optional actual = managerWithSerializable.get(prepareGet(0, 0, namespace1, TABLE_1)); - - assertThat(actual.isPresent()).isTrue(); - assertThat(getBalance(actual.get())).isEqualTo(expected); - } + assertThat(coordinator.getState(failingTxn.getId()).get().getState()) + .isEqualTo(TransactionState.ABORTED); + assertThat(coordinator.getState(successTxn.getId()).get().getState()) + .isEqualTo(TransactionState.COMMITTED); } @Test - public void manager_delete_DeleteGivenForExisting_WithSerializable_ShouldDeleteRecord() - throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - populateRecords(namespace1, TABLE_1); - Delete delete = prepareDelete(0, 0, namespace1, TABLE_1); + @EnabledIf("isGroupCommitEnabled") + void put_WhenAllTransactionsAbort_ShouldBeAbortedProperly() throws Exception { + // Act + ConsensusCommitManager manager = createConsensusCommitManager(Isolation.SERIALIZABLE); + DistributedTransaction failingTxn1 = manager.begin(); + DistributedTransaction failingTxn2 = manager.begin(); - // Act - managerWithSerializable.delete(delete); + doThrow(PreparationConflictException.class).when(commit).prepareRecords(any()); - // Assert - Optional result = managerWithSerializable.get(prepareGet(0, 0, namespace1, TABLE_1)); + failingTxn1.put(preparePut(0, 0, namespace1, TABLE_1)); + failingTxn2.put(preparePut(1, 0, namespace1, TABLE_1)); - assertThat(result.isPresent()).isFalse(); + try { + assertThat(catchThrowable(failingTxn1::commit)).isInstanceOf(CommitConflictException.class); + assertThat(catchThrowable(failingTxn2::commit)).isInstanceOf(CommitConflictException.class); + } finally { + reset(commit); } - } - - @Test - public void manager_mutate_WithSerializable_ShouldMutateRecords() throws TransactionException { - try (DistributedTransactionManager managerWithSerializable = - getTransactionManagerWithSerializable()) { - // Arrange - manager.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - manager.insert( - Insert.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 1)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, INITIAL_BALANCE) - .build()); - - Update update = - Update.newBuilder() - .namespace(namespace1) - .table(TABLE_1) - .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) - .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) - .intValue(BALANCE, 1) - .build(); - Delete delete = prepareDelete(1, 0, namespace1, TABLE_1); - - // Act - managerWithSerializable.mutate(Arrays.asList(update, delete)); - - // Assert - Optional result1 = managerWithSerializable.get(prepareGet(0, 0, namespace1, TABLE_1)); - Optional result2 = managerWithSerializable.get(prepareGet(1, 0, namespace1, TABLE_1)); - assertThat(result1.isPresent()).isTrue(); - assertThat(getBalance(result1.get())).isEqualTo(1); + // Assert + DistributedTransaction validationTxn = manager.beginReadOnly(); + assertThat(validationTxn.get(prepareGet(0, 0, namespace1, TABLE_1))).isEmpty(); + assertThat(validationTxn.get(prepareGet(1, 0, namespace1, TABLE_1))).isEmpty(); + validationTxn.commit(); - assertThat(result2.isPresent()).isFalse(); - } + assertThat(coordinator.getState(failingTxn1.getId()).get().getState()) + .isEqualTo(TransactionState.ABORTED); + assertThat(coordinator.getState(failingTxn2.getId()).get().getState()) + .isEqualTo(TransactionState.ABORTED); } private DistributedTransaction prepareTransfer( + ConsensusCommitManager manager, int fromId, String fromNamespace, String fromTable, @@ -6832,6 +7658,7 @@ private DistributedTransaction prepareTransfer( } private DistributedTransaction prepareDeletes( + ConsensusCommitManager manager, int one, String namespace, String table, @@ -6857,21 +7684,34 @@ private DistributedTransaction prepareDeletes( return transaction; } - private void populateRecords(String namespace, String table) throws TransactionException { + private void populateRecord(ConsensusCommitManager manager, String namespace, String table) + throws TransactionException { + manager.insert( + Insert.newBuilder() + .namespace(namespace) + .table(table) + .partitionKey(Key.ofInt(ACCOUNT_ID, 0)) + .clusteringKey(Key.ofInt(ACCOUNT_TYPE, 0)) + .intValue(BALANCE, INITIAL_BALANCE) + .build()); + } + + private void populateRecords(ConsensusCommitManager manager, String namespace, String table) + throws TransactionException { DistributedTransaction transaction = manager.begin(); for (int i = 0; i < NUM_ACCOUNTS; i++) { for (int j = 0; j < NUM_TYPES; j++) { Key partitionKey = Key.ofInt(ACCOUNT_ID, i); Key clusteringKey = Key.ofInt(ACCOUNT_TYPE, j); - Put put = - Put.newBuilder() + Insert insert = + Insert.newBuilder() .namespace(namespace) .table(table) .partitionKey(partitionKey) .clusteringKey(clusteringKey) .intValue(BALANCE, INITIAL_BALANCE) .build(); - transaction.put(put); + transaction.insert(insert); } } transaction.commit(); @@ -7038,11 +7878,88 @@ private int getBalance(Result result) { return balance.get().getAsInt(); } - private DistributedTransactionManager getTransactionManagerWithSerializable() { - Properties properties = getProperties(TEST_NAME); - // Add testName as a coordinator namespace suffix - ConsensusCommitTestUtils.addSuffixToCoordinatorNamespace(properties, TEST_NAME); - properties.put(ConsensusCommitConfig.ISOLATION_LEVEL, Isolation.SERIALIZABLE.name()); - return TransactionFactory.create(properties).getTransactionManager(); + private ConsensusCommitManager createConsensusCommitManager(Isolation isolation) { + storage = spy(originalStorage); + coordinator = spy(new Coordinator(storage, consensusCommitConfig)); + TransactionTableMetadataManager tableMetadataManager = + new TransactionTableMetadataManager(admin, -1); + recovery = spy(new RecoveryHandler(storage, coordinator, tableMetadataManager)); + recoveryExecutor = new RecoveryExecutor(coordinator, recovery, tableMetadataManager); + groupCommitter = CoordinatorGroupCommitter.from(consensusCommitConfig).orElse(null); + commit = spy(createCommitHandler(tableMetadataManager, groupCommitter)); + return new ConsensusCommitManager( + storage, + admin, + databaseConfig, + coordinator, + parallelExecutor, + recoveryExecutor, + commit, + isolation, + false, + groupCommitter); + } + + private CommitHandler createCommitHandler( + TransactionTableMetadataManager tableMetadataManager, + @Nullable CoordinatorGroupCommitter groupCommitter) { + if (groupCommitter != null) { + return new CommitHandlerWithGroupCommit( + storage, coordinator, tableMetadataManager, parallelExecutor, true, groupCommitter); + } else { + return new CommitHandler(storage, coordinator, tableMetadataManager, parallelExecutor, true); + } + } + + private DistributedTransaction begin(ConsensusCommitManager manager, boolean readOnly) { + if (readOnly) { + return manager.beginReadOnly(); + } else { + return manager.begin(); + } + } + + private void waitForRecoveryCompletion(DistributedTransaction transaction) throws CrudException { + if (transaction instanceof DecoratedDistributedTransaction) { + transaction = ((DecoratedDistributedTransaction) transaction).getOriginalTransaction(); + } + assert transaction instanceof ConsensusCommit; + + ((ConsensusCommit) transaction).getCrudHandler().waitForRecoveryCompletion(); + } + + private boolean isGroupCommitEnabled() { + return consensusCommitConfig.isCoordinatorGroupCommitEnabled(); + } + + static Stream isolationAndReadOnlyMode() { + return Arrays.stream(Isolation.values()) + .flatMap( + isolation -> Stream.of(false, true).map(readOnly -> Arguments.of(isolation, readOnly))); + } + + static Stream isolationAndCommitType() { + return Arrays.stream(Isolation.values()) + .flatMap( + isolation -> + Arrays.stream(CommitType.values()) + .map(commitType -> Arguments.of(isolation, commitType))); + } + + static Stream isolationAndReadOnlyModeAndCommitType() { + return Arrays.stream(Isolation.values()) + .flatMap( + isolation -> + Stream.of(false, true) + .flatMap( + readOnly -> + Arrays.stream(CommitType.values()) + .map(commitType -> Arguments.of(isolation, readOnly, commitType)))); + } + + enum CommitType { + NORMAL_COMMIT, + GROUP_COMMIT, + DELAYED_GROUP_COMMIT } }