diff --git a/core/src/integration-test/java/com/scalar/db/storage/cassandra/ConsensusCommitCassandraEnv.java b/core/src/integration-test/java/com/scalar/db/storage/cassandra/ConsensusCommitCassandraEnv.java index 58211916fb..0babd16d1b 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cassandra/ConsensusCommitCassandraEnv.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cassandra/ConsensusCommitCassandraEnv.java @@ -1,6 +1,6 @@ package com.scalar.db.storage.cassandra; -import com.scalar.db.common.ConsensusCommitTestUtils; +import com.scalar.db.transaction.consensuscommit.ConsensusCommitTestUtils; import java.util.Properties; public final class ConsensusCommitCassandraEnv { diff --git a/core/src/integration-test/java/com/scalar/db/storage/cosmos/ConsensusCommitCosmosEnv.java b/core/src/integration-test/java/com/scalar/db/storage/cosmos/ConsensusCommitCosmosEnv.java index 17aa015dc1..9bc27a2b49 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/cosmos/ConsensusCommitCosmosEnv.java +++ b/core/src/integration-test/java/com/scalar/db/storage/cosmos/ConsensusCommitCosmosEnv.java @@ -1,6 +1,6 @@ package com.scalar.db.storage.cosmos; -import com.scalar.db.common.ConsensusCommitTestUtils; +import com.scalar.db.transaction.consensuscommit.ConsensusCommitTestUtils; import java.util.Map; import java.util.Properties; diff --git a/core/src/integration-test/java/com/scalar/db/storage/dynamo/ConsensusCommitDynamoEnv.java b/core/src/integration-test/java/com/scalar/db/storage/dynamo/ConsensusCommitDynamoEnv.java index 4d0e78e806..c057659578 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/dynamo/ConsensusCommitDynamoEnv.java +++ b/core/src/integration-test/java/com/scalar/db/storage/dynamo/ConsensusCommitDynamoEnv.java @@ -1,6 +1,6 @@ package com.scalar.db.storage.dynamo; -import com.scalar.db.common.ConsensusCommitTestUtils; +import com.scalar.db.transaction.consensuscommit.ConsensusCommitTestUtils; import java.util.Map; import java.util.Properties; diff --git a/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitJdbcEnv.java b/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitJdbcEnv.java index 9d19bbee11..f4d96fd905 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitJdbcEnv.java +++ b/core/src/integration-test/java/com/scalar/db/storage/jdbc/ConsensusCommitJdbcEnv.java @@ -1,6 +1,6 @@ package com.scalar.db.storage.jdbc; -import com.scalar.db.common.ConsensusCommitTestUtils; +import com.scalar.db.transaction.consensuscommit.ConsensusCommitTestUtils; import java.util.Properties; public final class ConsensusCommitJdbcEnv { diff --git a/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitNullMetadataIntegrationTestWithMultiStorage.java b/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitNullMetadataIntegrationTestWithMultiStorage.java index 99ae15494d..f5a2f2eddf 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitNullMetadataIntegrationTestWithMultiStorage.java +++ b/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitNullMetadataIntegrationTestWithMultiStorage.java @@ -1,8 +1,8 @@ package com.scalar.db.storage.multistorage; -import com.scalar.db.common.ConsensusCommitTestUtils; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.transaction.consensuscommit.ConsensusCommitNullMetadataIntegrationTestBase; +import com.scalar.db.transaction.consensuscommit.ConsensusCommitTestUtils; import com.scalar.db.transaction.consensuscommit.Coordinator; import java.util.Properties; diff --git a/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitSpecificIntegrationTestWithMultiStorage.java b/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitSpecificIntegrationTestWithMultiStorage.java index 9c1e703a98..8c99644080 100644 --- a/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitSpecificIntegrationTestWithMultiStorage.java +++ b/core/src/integration-test/java/com/scalar/db/storage/multistorage/ConsensusCommitSpecificIntegrationTestWithMultiStorage.java @@ -1,8 +1,8 @@ package com.scalar.db.storage.multistorage; -import com.scalar.db.common.ConsensusCommitTestUtils; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.transaction.consensuscommit.ConsensusCommitSpecificIntegrationTestBase; +import com.scalar.db.transaction.consensuscommit.ConsensusCommitTestUtils; import com.scalar.db.transaction.consensuscommit.Coordinator; import java.util.Properties; diff --git a/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionIntegrationTest.java b/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionIntegrationTest.java index 951ea6a57a..37bebaf726 100644 --- a/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionIntegrationTest.java +++ b/core/src/integration-test/java/com/scalar/db/transaction/jdbc/JdbcTransactionIntegrationTest.java @@ -6,6 +6,7 @@ import com.scalar.db.storage.jdbc.JdbcEnv; import java.util.Properties; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; public class JdbcTransactionIntegrationTest extends DistributedTransactionIntegrationTestBase { @@ -24,17 +25,21 @@ protected Properties getProperties(String testName) { @Disabled("JDBC transactions don't support getState()") @Override + @Test public void getState_forSuccessfulTransaction_ShouldReturnCommittedState() {} @Disabled("JDBC transactions don't support getState()") @Override + @Test public void getState_forFailedTransaction_ShouldReturnAbortedState() {} @Disabled("JDBC transactions don't support abort()") @Override + @Test public void abort_forOngoingTransaction_ShouldAbortCorrectly() {} @Disabled("JDBC transactions don't support rollback()") @Override + @Test public void rollback_forOngoingTransaction_ShouldRollbackCorrectly() {} } diff --git a/core/src/main/java/com/scalar/db/api/CrudOperable.java b/core/src/main/java/com/scalar/db/api/CrudOperable.java index a773f3e93a..ecbf67dd28 100644 --- a/core/src/main/java/com/scalar/db/api/CrudOperable.java +++ b/core/src/main/java/com/scalar/db/api/CrudOperable.java @@ -12,6 +12,9 @@ * An interface for transactional CRUD operations. Note that the LINEARIZABLE consistency level is * always used in transactional CRUD operations, so {@link Consistency} specified for CRUD * operations is ignored. + * + * @param the type of {@link TransactionException} that the implementation throws if the + * operation fails */ public interface CrudOperable { @@ -26,9 +29,18 @@ public interface CrudOperable { Optional get(Get get) throws E; /** - * Retrieves results from the storage through a transaction with the specified {@link Scan} - * command with a partition key and returns a list of {@link Result}. Results can be filtered by - * specifying a range of clustering keys. + * Retrieves results from the storage through a transaction with the specified {@link Scan} or + * {@link ScanAll} or {@link ScanWithIndex} command with a partition key and returns a list of + * {@link Result}. Results can be filtered by specifying a range of clustering keys. + * + *
    + *
  • {@link Scan} : by specifying a partition key, it will return results within the + * partition. Results can be filtered by specifying a range of clustering keys. + *
  • {@link ScanAll} : for a given table, it will return all its records even if they span + * several partitions. + *
  • {@link ScanWithIndex} : by specifying an index key, it will return results within the + * index. + *
* * @param scan a {@code Scan} command * @return a list of {@link Result} @@ -36,6 +48,18 @@ public interface CrudOperable { */ List scan(Scan scan) throws E; + /** + * Retrieves results from the storage through a transaction with the specified {@link Scan} or + * {@link ScanAll} or {@link ScanWithIndex} command with a partition key and returns a {@link + * Scanner} to iterate over the results. Results can be filtered by specifying a range of + * clustering keys. + * + * @param scan a {@code Scan} command + * @return a {@code Scanner} to iterate over the results + * @throws E if the transaction CRUD operation fails + */ + Scanner getScanner(Scan scan) throws E; + /** * Inserts an entry into or updates an entry in the underlying storage through a transaction with * the specified {@link Put} command. If a condition is specified in the {@link Put} command, and @@ -131,4 +155,32 @@ public interface CrudOperable { * @throws E if the transaction CRUD operation fails */ void mutate(List mutations) throws E; + + /** A scanner abstraction for iterating results. */ + interface Scanner extends AutoCloseable, Iterable { + /** + * Returns the next result. + * + * @return an {@code Optional} containing the next result if available, or empty if no more + * results + * @throws E if the operation fails + */ + Optional one() throws E; + + /** + * Returns all remaining results. + * + * @return a {@code List} containing all remaining results + * @throws E if the operation fails + */ + List all() throws E; + + /** + * Closes the scanner. + * + * @throws E if closing the scanner fails + */ + @Override + void close() throws E; + } } diff --git a/core/src/main/java/com/scalar/db/api/Scanner.java b/core/src/main/java/com/scalar/db/api/Scanner.java index 21a9b3a7cc..b863b53480 100644 --- a/core/src/main/java/com/scalar/db/api/Scanner.java +++ b/core/src/main/java/com/scalar/db/api/Scanner.java @@ -13,17 +13,18 @@ public interface Scanner extends Closeable, Iterable { /** - * Returns the first result in the results. + * Returns the next result. * - * @return the first result in the results + * @return an {@code Optional} containing the next result if available, or empty if no more + * results * @throws ExecutionException if the operation fails */ Optional one() throws ExecutionException; /** - * Returns all the results. + * Returns all remaining results. * - * @return the list of {@code Result}s + * @return a {@code List} containing all remaining results * @throws ExecutionException if the operation fails */ List all() throws ExecutionException; diff --git a/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java b/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java index c8303f7a90..d2be32919a 100644 --- a/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java +++ b/core/src/main/java/com/scalar/db/api/TransactionCrudOperable.java @@ -33,6 +33,18 @@ public interface TransactionCrudOperable extends CrudOperable { @Override List scan(Scan scan) throws CrudConflictException, CrudException; + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or nontransient + * faults. You can try retrying the transaction from the beginning, but the transaction may + * still fail if the cause is nontranient + */ + @Override + Scanner getScanner(Scan scan) throws CrudConflictException, CrudException; + /** * {@inheritDoc} * @@ -154,4 +166,38 @@ void delete(List deletes) @Override void mutate(List mutations) throws CrudConflictException, CrudException, UnsatisfiedConditionException; + + interface Scanner extends CrudOperable.Scanner { + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or + * nontransient faults. You can try retrying the transaction from the beginning, but the + * transaction may still fail if the cause is nontranient + */ + @Override + Optional one() throws CrudConflictException, CrudException; + + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or + * nontransient faults. You can try retrying the transaction from the beginning, but the + * transaction may still fail if the cause is nontranient + */ + @Override + List all() throws CrudConflictException, CrudException; + + /** + * {@inheritDoc} + * + * @throws CrudException if closing the scanner fails + */ + @Override + void close() throws CrudException; + } } diff --git a/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java b/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java index eb285a2d54..608d80cdf3 100644 --- a/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java +++ b/core/src/main/java/com/scalar/db/api/TransactionManagerCrudOperable.java @@ -39,6 +39,18 @@ Optional get(Get get) List scan(Scan scan) throws CrudConflictException, CrudException, UnknownTransactionStatusException; + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or nontransient + * faults. You can try retrying the transaction from the beginning, but the transaction may + * still fail if the cause is nontranient + */ + @Override + Scanner getScanner(Scan scan) throws CrudConflictException, CrudException; + /** * {@inheritDoc} * @@ -177,4 +189,39 @@ void delete(List deletes) void mutate(List mutations) throws CrudConflictException, CrudException, UnsatisfiedConditionException, UnknownTransactionStatusException; + + interface Scanner extends CrudOperable.Scanner { + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or + * nontransient faults. You can try retrying the transaction from the beginning, but the + * transaction may still fail if the cause is nontranient + */ + @Override + Optional one() throws CrudConflictException, CrudException; + + /** + * {@inheritDoc} + * + * @throws CrudConflictException if the transaction CRUD operation fails due to transient faults + * (e.g., a conflict error). You can retry the transaction from the beginning + * @throws CrudException if the transaction CRUD operation fails due to transient or + * nontransient faults. You can try retrying the transaction from the beginning, but the + * transaction may still fail if the cause is nontranient + */ + @Override + List all() throws CrudConflictException, CrudException; + + /** + * {@inheritDoc} + * + * @throws CrudException if closing the scanner fails + * @throws UnknownTransactionStatusException if the status of the commit is unknown + */ + @Override + void close() throws CrudException, UnknownTransactionStatusException; + } } diff --git a/core/src/main/java/com/scalar/db/common/AbstractCrudOperableScanner.java b/core/src/main/java/com/scalar/db/common/AbstractCrudOperableScanner.java new file mode 100644 index 0000000000..1e5a6fc6be --- /dev/null +++ b/core/src/main/java/com/scalar/db/common/AbstractCrudOperableScanner.java @@ -0,0 +1,61 @@ +package com.scalar.db.common; + +import com.google.errorprone.annotations.concurrent.LazyInit; +import com.scalar.db.api.CrudOperable; +import com.scalar.db.api.Result; +import com.scalar.db.exception.transaction.TransactionException; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; + +public abstract class AbstractCrudOperableScanner + implements CrudOperable.Scanner { + + @LazyInit private ScannerIterator scannerIterator; + + @Override + @Nonnull + public Iterator iterator() { + if (scannerIterator == null) { + scannerIterator = new ScannerIterator(this); + } + return scannerIterator; + } + + @NotThreadSafe + public class ScannerIterator implements Iterator { + + private final CrudOperable.Scanner scanner; + private Result next; + + public ScannerIterator(CrudOperable.Scanner scanner) { + this.scanner = Objects.requireNonNull(scanner); + } + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + + try { + return (next = scanner.one().orElse(null)) != null; + } catch (TransactionException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public Result next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + Result ret = next; + next = null; + return ret; + } + } +} diff --git a/core/src/main/java/com/scalar/db/common/AbstractTransactionCrudOperableScanner.java b/core/src/main/java/com/scalar/db/common/AbstractTransactionCrudOperableScanner.java new file mode 100644 index 0000000000..34e1d54568 --- /dev/null +++ b/core/src/main/java/com/scalar/db/common/AbstractTransactionCrudOperableScanner.java @@ -0,0 +1,7 @@ +package com.scalar.db.common; + +import com.scalar.db.api.TransactionCrudOperable; +import com.scalar.db.exception.transaction.CrudException; + +public abstract class AbstractTransactionCrudOperableScanner + extends AbstractCrudOperableScanner implements TransactionCrudOperable.Scanner {} diff --git a/core/src/main/java/com/scalar/db/common/AbstractTransactionManagerCrudOperableScanner.java b/core/src/main/java/com/scalar/db/common/AbstractTransactionManagerCrudOperableScanner.java new file mode 100644 index 0000000000..5dcbdb3479 --- /dev/null +++ b/core/src/main/java/com/scalar/db/common/AbstractTransactionManagerCrudOperableScanner.java @@ -0,0 +1,8 @@ +package com.scalar.db.common; + +import com.scalar.db.api.TransactionManagerCrudOperable; +import com.scalar.db.exception.transaction.TransactionException; + +public abstract class AbstractTransactionManagerCrudOperableScanner + extends AbstractCrudOperableScanner + implements TransactionManagerCrudOperable.Scanner {} diff --git a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java index e3ef02c635..ea592e5b41 100644 --- a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java @@ -121,6 +121,11 @@ public synchronized List scan(Scan scan) throws CrudException { return super.scan(scan); } + @Override + public synchronized Scanner getScanner(Scan scan) throws CrudException { + return super.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java index a6a4c6b9ee..b0543433d3 100644 --- a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java @@ -127,6 +127,11 @@ public synchronized List scan(Scan scan) throws CrudException { return super.scan(scan); } + @Override + public synchronized Scanner getScanner(Scan scan) throws CrudException { + return super.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java index ca6d0cdd76..ca997988c6 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransaction.java @@ -78,6 +78,11 @@ public List scan(Scan scan) throws CrudException { return transaction.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + return transaction.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java index c2caadd9eb..dac3cfa2c7 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java @@ -157,6 +157,11 @@ public List scan(Scan scan) throws CrudException, UnknownTransactionStat return transactionManager.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + return transactionManager.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java index 097e4f032c..04a48f4314 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransaction.java @@ -80,6 +80,11 @@ public List scan(Scan scan) throws CrudException { return transaction.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + return transaction.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java index edcbd6e7a6..ce479795f1 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedTwoPhaseCommitTransactionManager.java @@ -111,6 +111,11 @@ public List scan(Scan scan) throws CrudException, UnknownTransactionStat return transactionManager.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + return transactionManager.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java b/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java index 7d7adec72e..52866cb405 100644 --- a/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java @@ -70,6 +70,12 @@ public List scan(Scan scan) throws CrudException { return super.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + checkIfActive(); + return super.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java b/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java index 7ba79ddede..1d79240d04 100644 --- a/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java @@ -76,6 +76,12 @@ public List scan(Scan scan) throws CrudException { return super.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + checkIfActive(); + return super.getScanner(scan); + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/error/CoreError.java b/core/src/main/java/com/scalar/db/common/error/CoreError.java index acaa4566dd..7c5a5fd1d4 100644 --- a/core/src/main/java/com/scalar/db/common/error/CoreError.java +++ b/core/src/main/java/com/scalar/db/common/error/CoreError.java @@ -911,6 +911,18 @@ public enum CoreError implements ScalarDbError { Category.USER_ERROR, "0203", "Delimiter must not be null", "", ""), DATA_LOADER_CONFIG_FILE_PATH_BLANK( Category.USER_ERROR, "0204", "Config file path must not be blank", "", ""), + CONSENSUS_COMMIT_SCANNER_NOT_CLOSED( + Category.USER_ERROR, + "0205", + "Some scanners were not closed. All scanners must be closed before committing the transaction.", + "", + ""), + TWO_PHASE_CONSENSUS_COMMIT_SCANNER_NOT_CLOSED( + Category.USER_ERROR, + "0206", + "Some scanners were not closed. All scanners must be closed before preparing the transaction.", + "", + ""), // // Errors for the concurrency error category @@ -1182,6 +1194,8 @@ public enum CoreError implements ScalarDbError { Category.INTERNAL_ERROR, "0052", "Failed to read JSON file. Details: %s.", "", ""), DATA_LOADER_JSONLINES_FILE_READ_FAILED( Category.INTERNAL_ERROR, "0053", "Failed to read JSON Lines file. Details: %s.", "", ""), + JDBC_TRANSACTION_GETTING_SCANNER_FAILED( + Category.INTERNAL_ERROR, "0054", "Getting the scanner failed. Details: %s", "", ""), // // Errors for the unknown transaction status error category diff --git a/core/src/main/java/com/scalar/db/service/TransactionService.java b/core/src/main/java/com/scalar/db/service/TransactionService.java index fd68d2dc22..8acc748eaa 100644 --- a/core/src/main/java/com/scalar/db/service/TransactionService.java +++ b/core/src/main/java/com/scalar/db/service/TransactionService.java @@ -167,11 +167,20 @@ public List scan(Scan scan) throws CrudException, UnknownTransactionStat return manager.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + return manager.getScanner(scan); + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated @Override public void put(Put put) throws CrudException, UnknownTransactionStatusException { manager.put(put); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated @Override public void put(List puts) throws CrudException, UnknownTransactionStatusException { manager.put(puts); @@ -197,6 +206,8 @@ public void delete(Delete delete) throws CrudException, UnknownTransactionStatus manager.delete(delete); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated @Override public void delete(List deletes) throws CrudException, UnknownTransactionStatusException { manager.delete(deletes); diff --git a/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java b/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java index a2a2439f2c..6cf6f68a98 100644 --- a/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java +++ b/core/src/main/java/com/scalar/db/service/TwoPhaseCommitTransactionService.java @@ -124,11 +124,20 @@ public List scan(Scan scan) throws CrudException, UnknownTransactionStat return manager.scan(scan); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + return manager.getScanner(scan); + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated @Override public void put(Put put) throws CrudException, UnknownTransactionStatusException { manager.put(put); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated @Override public void put(List puts) throws CrudException, UnknownTransactionStatusException { manager.put(puts); @@ -154,6 +163,8 @@ public void delete(Delete delete) throws CrudException, UnknownTransactionStatus manager.delete(delete); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated @Override public void delete(List deletes) throws CrudException, UnknownTransactionStatusException { manager.delete(deletes); diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcService.java b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcService.java index 82698ba5c9..852812ac94 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcService.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcService.java @@ -96,9 +96,14 @@ public Optional get(Get get, Connection connection) } } - @SuppressFBWarnings("OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE") public Scanner getScanner(Scan scan, Connection connection) throws SQLException, ExecutionException { + return getScanner(scan, connection, true); + } + + @SuppressFBWarnings("OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE") + public Scanner getScanner(Scan scan, Connection connection, boolean closeConnectionOnScannerClose) + throws SQLException, ExecutionException { operationChecker.check(scan); TableMetadata tableMetadata = tableMetadataManager.getTableMetadata(scan); @@ -111,7 +116,8 @@ public Scanner getScanner(Scan scan, Connection connection) new ResultInterpreter(scan.getProjections(), tableMetadata, rdbEngine), connection, preparedStatement, - resultSet); + resultSet, + closeConnectionOnScannerClose); } public List scan(Scan scan, Connection connection) diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/ScannerImpl.java b/core/src/main/java/com/scalar/db/storage/jdbc/ScannerImpl.java index 3d48e2a2f9..d9dc38268e 100644 --- a/core/src/main/java/com/scalar/db/storage/jdbc/ScannerImpl.java +++ b/core/src/main/java/com/scalar/db/storage/jdbc/ScannerImpl.java @@ -25,17 +25,20 @@ public class ScannerImpl extends AbstractScanner { private final Connection connection; private final PreparedStatement preparedStatement; private final ResultSet resultSet; + private final boolean closeConnectionOnClose; @SuppressFBWarnings("EI_EXPOSE_REP2") public ScannerImpl( ResultInterpreter resultInterpreter, Connection connection, PreparedStatement preparedStatement, - ResultSet resultSet) { + ResultSet resultSet, + boolean closeConnectionOnClose) { this.resultInterpreter = Objects.requireNonNull(resultInterpreter); this.connection = Objects.requireNonNull(connection); this.preparedStatement = Objects.requireNonNull(preparedStatement); this.resultSet = Objects.requireNonNull(resultSet); + this.closeConnectionOnClose = closeConnectionOnClose; } @Override @@ -75,10 +78,13 @@ public void close() { } catch (SQLException e) { logger.warn("Failed to close the preparedStatement", e); } - try { - connection.close(); - } catch (SQLException e) { - logger.warn("Failed to close the connection", e); + + if (closeConnectionOnClose) { + try { + connection.close(); + } catch (SQLException e) { + logger.warn("Failed to close the connection", e); + } } } } 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 b215ccc928..c2f6f12797 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 @@ -24,8 +24,10 @@ import com.scalar.db.exception.transaction.UnsatisfiedConditionException; import com.scalar.db.util.ScalarDbUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Iterator; import java.util.List; import java.util.Optional; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import org.slf4j.Logger; @@ -96,6 +98,45 @@ public List scan(Scan scan) throws CrudException { } } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + scan = copyAndSetTargetToIfNot(scan); + Scanner scanner = crud.getScanner(scan); + + return new Scanner() { + @Override + public Optional one() throws CrudException { + try { + return scanner.one(); + } catch (UncommittedRecordException e) { + lazyRecovery(e); + throw e; + } + } + + @Override + public List all() throws CrudException { + try { + return scanner.all(); + } catch (UncommittedRecordException e) { + lazyRecovery(e); + throw e; + } + } + + @Override + public void close() throws CrudException { + scanner.close(); + } + + @Nonnull + @Override + public Iterator iterator() { + return scanner.iterator(); + } + }; + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override @@ -208,6 +249,10 @@ public void mutate(List mutations) throws CrudException { @Override public void commit() throws CommitException, UnknownTransactionStatusException { + if (!crud.areAllScannersClosed()) { + throw new IllegalStateException(CoreError.CONSENSUS_COMMIT_SCANNER_NOT_CLOSED.buildMessage()); + } + // Execute implicit pre-read try { crud.readIfImplicitPreReadEnabled(); @@ -229,6 +274,12 @@ public void commit() throws CommitException, UnknownTransactionStatusException { @Override public void rollback() { + try { + crud.closeScanners(); + } catch (CrudException e) { + logger.warn("Failed to close the scanner", e); + } + if (groupCommitter != null) { groupCommitter.remove(crud.getSnapshot().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 93e2056065..9eff205532 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 @@ -16,10 +16,12 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; import com.scalar.db.common.AbstractDistributedTransactionManager; +import com.scalar.db.common.AbstractTransactionManagerCrudOperableScanner; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.transaction.CommitConflictException; import com.scalar.db.exception.transaction.CrudConflictException; @@ -34,6 +36,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import org.slf4j.Logger; @@ -229,6 +232,88 @@ public List scan(Scan scan) throws CrudException, UnknownTransactionStat return executeTransaction(t -> t.scan(copyAndSetTargetToIfNot(scan))); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + DistributedTransaction transaction = begin(); + + TransactionCrudOperable.Scanner scanner; + try { + scanner = transaction.getScanner(copyAndSetTargetToIfNot(scan)); + } catch (CrudException e) { + rollbackTransaction(transaction); + throw e; + } + + return new AbstractTransactionManagerCrudOperableScanner() { + + private final AtomicBoolean closed = new AtomicBoolean(); + + @Override + public Optional one() throws CrudException { + try { + return scanner.one(); + } catch (CrudException e) { + closed.set(true); + + try { + scanner.close(); + } catch (CrudException ex) { + e.addSuppressed(ex); + } + + rollbackTransaction(transaction); + throw e; + } + } + + @Override + public List all() throws CrudException { + try { + return scanner.all(); + } catch (CrudException e) { + closed.set(true); + + try { + scanner.close(); + } catch (CrudException ex) { + e.addSuppressed(ex); + } + + rollbackTransaction(transaction); + throw e; + } + } + + @Override + public void close() throws CrudException, UnknownTransactionStatusException { + if (closed.get()) { + return; + } + closed.set(true); + + try { + scanner.close(); + } catch (CrudException e) { + rollbackTransaction(transaction); + throw e; + } + + try { + transaction.commit(); + } catch (CommitConflictException e) { + rollbackTransaction(transaction); + throw new CrudConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } catch (UnknownTransactionStatusException e) { + throw e; + } catch (TransactionException e) { + rollbackTransaction(transaction); + throw new CrudException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } + } + }; + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void put(Put put) throws CrudException, UnknownTransactionStatusException { @@ -239,6 +324,7 @@ public void put(Put put) throws CrudException, UnknownTransactionStatusException }); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void put(List puts) throws CrudException, UnknownTransactionStatusException { @@ -285,6 +371,7 @@ public void delete(Delete delete) throws CrudException, UnknownTransactionStatus }); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void delete(List deletes) throws CrudException, UnknownTransactionStatusException { 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 c66595ca81..ebe5da6a13 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 @@ -16,6 +16,8 @@ import com.scalar.db.api.Scanner; import com.scalar.db.api.Selection; import com.scalar.db.api.TableMetadata; +import com.scalar.db.api.TransactionCrudOperable; +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.CrudException; @@ -23,10 +25,13 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.util.ArrayList; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @@ -42,6 +47,7 @@ public class CrudHandler { private final boolean isIncludeMetadataEnabled; private final MutationConditionsValidator mutationConditionsValidator; private final ParallelExecutor parallelExecutor; + private final List scanners = new ArrayList<>(); @SuppressFBWarnings("EI_EXPOSE_REP2") public CrudHandler( @@ -211,6 +217,37 @@ private void processScanResult(Snapshot.Key key, Scan scan, TransactionResult re snapshot.putIntoReadSet(key, Optional.of(result)); } + public TransactionCrudOperable.Scanner getScanner(Scan originalScan) throws CrudException { + List originalProjections = new ArrayList<>(originalScan.getProjections()); + Scan scan = (Scan) prepareStorageSelection(originalScan); + + ConsensusCommitScanner scanner; + + Optional> resultsInSnapshot = + snapshot.getResults(scan); + if (resultsInSnapshot.isPresent()) { + scanner = + new ConsensusCommitSnapshotScanner(scan, originalProjections, resultsInSnapshot.get()); + } else { + scanner = new ConsensusCommitStorageScanner(scan, originalProjections); + } + + scanners.add(scanner); + return scanner; + } + + public boolean areAllScannersClosed() { + return scanners.stream().allMatch(ConsensusCommitScanner::isClosed); + } + + public void closeScanners() throws CrudException { + for (ConsensusCommitScanner scanner : scanners) { + if (!scanner.isClosed()) { + scanner.close(); + } + } + } + public void put(Put put) throws CrudException { Snapshot.Key key = new Snapshot.Key(put); @@ -360,4 +397,169 @@ private TableMetadata getTableMetadata(Operation operation) throws CrudException public Snapshot getSnapshot() { return snapshot; } + + private interface ConsensusCommitScanner extends TransactionCrudOperable.Scanner { + boolean isClosed(); + } + + @NotThreadSafe + private class ConsensusCommitStorageScanner extends AbstractTransactionCrudOperableScanner + implements ConsensusCommitScanner { + + private final Scan scan; + private final List originalProjections; + private final Scanner scanner; + + private final LinkedHashMap results = new LinkedHashMap<>(); + private final AtomicBoolean fullyScanned = new AtomicBoolean(); + private final AtomicBoolean closed = new AtomicBoolean(); + + public ConsensusCommitStorageScanner(Scan scan, List originalProjections) + throws CrudException { + this.scan = scan; + this.originalProjections = originalProjections; + scanner = scanFromStorage(scan); + } + + @Override + public Optional one() throws CrudException { + try { + Optional r = scanner.one(); + + if (!r.isPresent()) { + fullyScanned.set(true); + return Optional.empty(); + } + + Snapshot.Key key = new Snapshot.Key(scan, r.get()); + TransactionResult result = new TransactionResult(r.get()); + processScanResult(key, scan, result); + results.put(key, result); + + TableMetadata metadata = getTableMetadata(scan); + return Optional.of( + new FilteredResult(result, originalProjections, metadata, isIncludeMetadataEnabled)); + } catch (ExecutionException e) { + closeScanner(); + throw new CrudException( + CoreError.CONSENSUS_COMMIT_SCANNING_RECORDS_FROM_STORAGE_FAILED.buildMessage(), + e, + snapshot.getId()); + } catch (CrudException e) { + closeScanner(); + throw e; + } + } + + @Override + public List all() throws CrudException { + List results = new ArrayList<>(); + + while (true) { + Optional result = one(); + if (!result.isPresent()) { + break; + } + results.add(result.get()); + } + + return results; + } + + @Override + public void close() { + if (closed.get()) { + return; + } + + closeScanner(); + + if (fullyScanned.get()) { + // If the scanner is fully scanned, we can treat it as a normal scan, and put the results + // into the scan set + snapshot.putIntoScanSet(scan, results); + } else { + // If the scanner is not fully scanned, put the results into the scanner set + snapshot.putIntoScannerSet(scan, results); + } + + snapshot.verifyNoOverlap(scan, results); + } + + @Override + public boolean isClosed() { + return closed.get(); + } + + private void closeScanner() { + closed.set(true); + try { + scanner.close(); + } catch (IOException e) { + logger.warn("Failed to close the scanner", e); + } + } + } + + @NotThreadSafe + private class ConsensusCommitSnapshotScanner extends AbstractTransactionCrudOperableScanner + implements ConsensusCommitScanner { + + private final Scan scan; + private final List originalProjections; + private final Iterator> resultsIterator; + + private final LinkedHashMap results = new LinkedHashMap<>(); + private boolean closed; + + public ConsensusCommitSnapshotScanner( + Scan scan, + List originalProjections, + LinkedHashMap resultsInSnapshot) { + this.scan = scan; + this.originalProjections = originalProjections; + resultsIterator = resultsInSnapshot.entrySet().iterator(); + } + + @Override + public Optional one() throws CrudException { + if (!resultsIterator.hasNext()) { + return Optional.empty(); + } + + Map.Entry entry = resultsIterator.next(); + results.put(entry.getKey(), entry.getValue()); + + TableMetadata metadata = getTableMetadata(scan); + return Optional.of( + new FilteredResult( + entry.getValue(), originalProjections, metadata, isIncludeMetadataEnabled)); + } + + @Override + public List all() throws CrudException { + List results = new ArrayList<>(); + + while (true) { + Optional result = one(); + if (!result.isPresent()) { + break; + } + results.add(result.get()); + } + + return results; + } + + @Override + public void close() { + closed = true; + snapshot.verifyNoOverlap(scan, results); + } + + @Override + public boolean isClosed() { + return closed; + } + } } 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 cf535281ab..24b16e59c7 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 @@ -64,6 +64,9 @@ public class Snapshot { private final Map writeSet; private final Map deleteSet; + // The scanner set used to store information about scanners that are not fully scanned + private final List scannerSet; + public Snapshot( String id, Isolation isolation, @@ -78,6 +81,7 @@ public Snapshot( scanSet = new HashMap<>(); writeSet = new HashMap<>(); deleteSet = new HashMap<>(); + scannerSet = new ArrayList<>(); } @VisibleForTesting @@ -90,7 +94,8 @@ public Snapshot( ConcurrentMap> getSet, Map> scanSet, Map writeSet, - Map deleteSet) { + Map deleteSet, + List scannerSet) { this.id = id; this.isolation = isolation; this.tableMetadataManager = tableMetadataManager; @@ -100,6 +105,7 @@ public Snapshot( this.scanSet = scanSet; this.writeSet = writeSet; this.deleteSet = deleteSet; + this.scannerSet = scannerSet; } @Nonnull @@ -173,6 +179,10 @@ public void putIntoDeleteSet(Key key, Delete delete) { deleteSet.put(key, delete); } + public void putIntoScannerSet(Scan scan, LinkedHashMap results) { + scannerSet.add(new ScannerInfo(scan, results)); + } + public List getPutsInWriteSet() { return new ArrayList<>(writeSet.values()); } @@ -485,7 +495,12 @@ void toSerializable(DistributedStorage storage) // Scan set is re-validated to check if there is no anti-dependency for (Map.Entry> entry : scanSet.entrySet()) { - tasks.add(() -> validateScanResults(storage, entry.getKey(), entry.getValue())); + tasks.add(() -> validateScanResults(storage, entry.getKey(), entry.getValue(), false)); + } + + // Scanner set is re-validated to check if there is no anti-dependency + for (ScannerInfo scannerInfo : scannerSet) { + tasks.add(() -> validateScanResults(storage, scannerInfo.scan, scannerInfo.results, true)); } // Get set is re-validated to check if there is no anti-dependency @@ -527,11 +542,16 @@ void toSerializable(DistributedStorage storage) * @param storage a distributed storage * @param scan the scan to be validated * @param results the results of the scan + * @param notFullyScannedScanner if this is a validation for a scanner that has not been fully + * scanned * @throws ExecutionException if a storage operation fails * @throws ValidationConflictException if the scan results are changed by another transaction */ private void validateScanResults( - DistributedStorage storage, Scan scan, LinkedHashMap results) + DistributedStorage storage, + Scan scan, + LinkedHashMap results, + boolean notFullyScannedScanner) throws ExecutionException, ValidationConflictException { Scanner scanner = null; try { @@ -604,6 +624,11 @@ private void validateScanResults( return; } + if (notFullyScannedScanner) { + // If the scanner is not fully scanned, no further checks are needed + return; + } + // Check if there are any remaining records in the latest scan results while (latestResult.isPresent()) { TransactionResult latestTxResult = new TransactionResult(latestResult.get()); @@ -654,7 +679,7 @@ private void validateGetWithIndexResult( originalResult.ifPresent(r -> results.put(new Snapshot.Key(scanWithIndex, r), r)); // Validate the result to check if there is no anti-dependency - validateScanResults(storage, scanWithIndex, results); + validateScanResults(storage, scanWithIndex, results, false); } private void validateGetResult( @@ -842,4 +867,32 @@ public int hashCode() { return Objects.hash(transactionId, readSetMap, writeSet, deleteSet); } } + + @VisibleForTesting + static class ScannerInfo { + public final Scan scan; + public final LinkedHashMap results; + + public ScannerInfo(Scan scan, LinkedHashMap results) { + this.scan = scan; + this.results = results; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ScannerInfo)) { + return false; + } + ScannerInfo that = (ScannerInfo) o; + return Objects.equals(scan, that.scan) && Objects.equals(results, that.results); + } + + @Override + public int hashCode() { + return Objects.hash(scan, results); + } + } } 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 fe25742dd7..dd72f03b4d 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 @@ -27,8 +27,10 @@ import com.scalar.db.exception.transaction.ValidationException; import com.scalar.db.util.ScalarDbUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Iterator; import java.util.List; import java.util.Optional; +import javax.annotation.Nonnull; import javax.annotation.concurrent.NotThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,6 +88,45 @@ public List scan(Scan scan) throws CrudException { } } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + scan = copyAndSetTargetToIfNot(scan); + Scanner scanner = crud.getScanner(scan); + + return new Scanner() { + @Override + public Optional one() throws CrudException { + try { + return scanner.one(); + } catch (UncommittedRecordException e) { + lazyRecovery(e); + throw e; + } + } + + @Override + public List all() throws CrudException { + try { + return scanner.all(); + } catch (UncommittedRecordException e) { + lazyRecovery(e); + throw e; + } + } + + @Override + public void close() throws CrudException { + scanner.close(); + } + + @Nonnull + @Override + public Iterator iterator() { + return scanner.iterator(); + } + }; + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override @@ -206,6 +247,11 @@ public void mutate(List mutations) throws CrudException { @Override public void prepare() throws PreparationException { + if (!crud.areAllScannersClosed()) { + throw new IllegalStateException( + CoreError.TWO_PHASE_CONSENSUS_COMMIT_SCANNER_NOT_CLOSED.buildMessage()); + } + // Execute implicit pre-read try { crud.readIfImplicitPreReadEnabled(); @@ -256,6 +302,12 @@ public void commit() throws CommitConflictException, UnknownTransactionStatusExc @Override public void rollback() throws RollbackException { + try { + crud.closeScanners(); + } catch (CrudException e) { + logger.warn("Failed to close the scanner", e); + } + if (!needRollback) { return; } diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java index 5afc3e98df..1097f0b62f 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManager.java @@ -13,10 +13,12 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.TwoPhaseCommitTransaction; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; +import com.scalar.db.common.AbstractTransactionManagerCrudOperableScanner; import com.scalar.db.common.AbstractTwoPhaseCommitTransactionManager; import com.scalar.db.common.error.CoreError; import com.scalar.db.config.DatabaseConfig; @@ -35,6 +37,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; import org.slf4j.Logger; @@ -186,6 +189,92 @@ public List scan(Scan scan) throws CrudException, UnknownTransactionStat return executeTransaction(t -> t.scan(copyAndSetTargetToIfNot(scan))); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + TwoPhaseCommitTransaction transaction = begin(); + + TransactionCrudOperable.Scanner scanner; + try { + scanner = transaction.getScanner(copyAndSetTargetToIfNot(scan)); + } catch (CrudException e) { + rollbackTransaction(transaction); + throw e; + } + + return new AbstractTransactionManagerCrudOperableScanner() { + + private final AtomicBoolean closed = new AtomicBoolean(); + + @Override + public Optional one() throws CrudException { + try { + return scanner.one(); + } catch (CrudException e) { + closed.set(true); + + try { + scanner.close(); + } catch (CrudException ex) { + e.addSuppressed(ex); + } + + rollbackTransaction(transaction); + throw e; + } + } + + @Override + public List all() throws CrudException { + try { + return scanner.all(); + } catch (CrudException e) { + closed.set(true); + + try { + scanner.close(); + } catch (CrudException ex) { + e.addSuppressed(ex); + } + + rollbackTransaction(transaction); + throw e; + } + } + + @Override + public void close() throws CrudException, UnknownTransactionStatusException { + if (closed.get()) { + return; + } + closed.set(true); + + try { + scanner.close(); + } catch (CrudException e) { + rollbackTransaction(transaction); + throw e; + } + + try { + transaction.prepare(); + transaction.validate(); + transaction.commit(); + } catch (PreparationConflictException + | ValidationConflictException + | CommitConflictException e) { + rollbackTransaction(transaction); + throw new CrudConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } catch (UnknownTransactionStatusException e) { + throw e; + } catch (TransactionException e) { + rollbackTransaction(transaction); + throw new CrudException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } + } + }; + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void put(Put put) throws CrudException, UnknownTransactionStatusException { @@ -196,6 +285,7 @@ public void put(Put put) throws CrudException, UnknownTransactionStatusException }); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void put(List puts) throws CrudException, UnknownTransactionStatusException { @@ -242,6 +332,7 @@ public void delete(Delete delete) throws CrudException, UnknownTransactionStatus }); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void delete(List deletes) throws CrudException, UnknownTransactionStatusException { diff --git a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java index 27e2cdc7f8..b29bf7ae31 100644 --- a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java +++ b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java @@ -20,6 +20,7 @@ import com.scalar.db.api.UpdateIfExists; import com.scalar.db.api.Upsert; import com.scalar.db.common.AbstractDistributedTransaction; +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.CommitConflictException; @@ -32,6 +33,7 @@ import com.scalar.db.storage.jdbc.JdbcService; import com.scalar.db.storage.jdbc.RdbEngineStrategy; import com.scalar.db.util.ScalarDbUtils; +import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -94,6 +96,50 @@ public List scan(Scan scan) throws CrudException { } } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + scan = copyAndSetTargetToIfNot(scan); + + com.scalar.db.api.Scanner scanner; + try { + scanner = jdbcService.getScanner(scan, connection, false); + } catch (SQLException e) { + throw createCrudException( + e, CoreError.JDBC_TRANSACTION_GETTING_SCANNER_FAILED.buildMessage(e.getMessage())); + } catch (ExecutionException e) { + throw new CrudException(e.getMessage(), e, txId); + } + + return new AbstractTransactionCrudOperableScanner() { + @Override + public Optional one() throws CrudException { + try { + return scanner.one(); + } catch (ExecutionException e) { + throw new CrudException(e.getMessage(), e, txId); + } + } + + @Override + public List all() throws CrudException { + try { + return scanner.all(); + } catch (ExecutionException e) { + throw new CrudException(e.getMessage(), e, txId); + } + } + + @Override + public void close() throws CrudException { + try { + scanner.close(); + } catch (IOException e) { + throw new CrudException(e.getMessage(), e, txId); + } + } + }; + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java index 551725b264..d65b512106 100644 --- a/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java +++ b/core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransactionManager.java @@ -12,10 +12,12 @@ import com.scalar.db.api.Result; import com.scalar.db.api.Scan; import com.scalar.db.api.SerializableStrategy; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; import com.scalar.db.common.AbstractDistributedTransactionManager; +import com.scalar.db.common.AbstractTransactionManagerCrudOperableScanner; import com.scalar.db.common.TableMetadataManager; import com.scalar.db.common.checker.OperationChecker; import com.scalar.db.common.error.CoreError; @@ -38,6 +40,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.concurrent.ThreadSafe; import org.apache.commons.dbcp2.BasicDataSource; import org.slf4j.Logger; @@ -168,6 +171,95 @@ public List scan(Scan scan) throws CrudException, UnknownTransactionStat return executeTransaction(t -> t.scan(copyAndSetTargetToIfNot(scan))); } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + DistributedTransaction transaction; + try { + transaction = begin(); + } catch (TransactionNotFoundException e) { + throw new CrudConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } catch (TransactionException e) { + throw new CrudException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } + + TransactionCrudOperable.Scanner scanner; + try { + scanner = transaction.getScanner(copyAndSetTargetToIfNot(scan)); + } catch (CrudException e) { + rollbackTransaction(transaction); + throw e; + } + + return new AbstractTransactionManagerCrudOperableScanner() { + + private final AtomicBoolean closed = new AtomicBoolean(); + + @Override + public Optional one() throws CrudException { + try { + return scanner.one(); + } catch (CrudException e) { + closed.set(true); + + try { + scanner.close(); + } catch (CrudException ex) { + e.addSuppressed(ex); + } + + rollbackTransaction(transaction); + throw e; + } + } + + @Override + public List all() throws CrudException { + try { + return scanner.all(); + } catch (CrudException e) { + closed.set(true); + + try { + scanner.close(); + } catch (CrudException ex) { + e.addSuppressed(ex); + } + + rollbackTransaction(transaction); + throw e; + } + } + + @Override + public void close() throws CrudException, UnknownTransactionStatusException { + if (closed.get()) { + return; + } + closed.set(true); + + try { + scanner.close(); + } catch (CrudException e) { + rollbackTransaction(transaction); + throw e; + } + + try { + transaction.commit(); + } catch (CommitConflictException e) { + rollbackTransaction(transaction); + throw new CrudConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } catch (UnknownTransactionStatusException e) { + throw e; + } catch (TransactionException e) { + rollbackTransaction(transaction); + throw new CrudException(e.getMessage(), e, e.getTransactionId().orElse(null)); + } + } + }; + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void put(Put put) throws CrudException, UnknownTransactionStatusException { @@ -178,6 +270,7 @@ public void put(Put put) throws CrudException, UnknownTransactionStatusException }); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void put(List puts) throws CrudException, UnknownTransactionStatusException { @@ -224,6 +317,7 @@ public void delete(Delete delete) throws CrudException, UnknownTransactionStatus }); } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override public void delete(List deletes) throws CrudException, UnknownTransactionStatusException { diff --git a/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java b/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java index cb488cfa55..cbc136d120 100644 --- a/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java +++ b/core/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManager.java @@ -20,7 +20,6 @@ import com.scalar.db.api.PutIf; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; -import com.scalar.db.api.Scanner; import com.scalar.db.api.SerializableStrategy; import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; @@ -28,6 +27,7 @@ import com.scalar.db.api.UpdateIfExists; import com.scalar.db.api.Upsert; import com.scalar.db.common.AbstractDistributedTransactionManager; +import com.scalar.db.common.AbstractTransactionManagerCrudOperableScanner; import com.scalar.db.common.error.CoreError; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.storage.ExecutionException; @@ -156,13 +156,55 @@ public Optional get(Get get) throws CrudException { public List scan(Scan scan) throws CrudException { scan = copyAndSetTargetToIfNot(scan); - try (Scanner scanner = storage.scan(scan.withConsistency(Consistency.LINEARIZABLE))) { + try (com.scalar.db.api.Scanner scanner = + storage.scan(scan.withConsistency(Consistency.LINEARIZABLE))) { return scanner.all(); } catch (ExecutionException | IOException e) { throw new CrudException(e.getMessage(), e, null); } } + @Override + public Scanner getScanner(Scan scan) throws CrudException { + scan = copyAndSetTargetToIfNot(scan); + + com.scalar.db.api.Scanner scanner; + try { + scanner = storage.scan(scan); + } catch (ExecutionException e) { + throw new CrudException(e.getMessage(), e, null); + } + + return new AbstractTransactionManagerCrudOperableScanner() { + @Override + public Optional one() throws CrudException { + try { + return scanner.one(); + } catch (ExecutionException e) { + throw new CrudException(e.getMessage(), e, null); + } + } + + @Override + public List all() throws CrudException { + try { + return scanner.all(); + } catch (ExecutionException e) { + throw new CrudException(e.getMessage(), e, null); + } + } + + @Override + public void close() throws CrudException { + try { + scanner.close(); + } catch (IOException e) { + throw new CrudException(e.getMessage(), e, null); + } + } + }; + } + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ @Deprecated @Override diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcDatabaseTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcDatabaseTest.java index 82b40061a6..7710f6a426 100644 --- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcDatabaseTest.java +++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcDatabaseTest.java @@ -98,7 +98,8 @@ public void whenGetOperationExecuted_shouldCallJdbcService() throws Exception { public void whenScanOperationExecutedAndScannerClosed_shouldCallJdbcService() throws Exception { // Arrange when(jdbcService.getScanner(any(), any())) - .thenReturn(new ScannerImpl(resultInterpreter, connection, preparedStatement, resultSet)); + .thenReturn( + new ScannerImpl(resultInterpreter, connection, preparedStatement, resultSet, true)); // Act Scan scan = new Scan(new Key("p1", "val")).forNamespace(NAMESPACE).forTable(TABLE); 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 4b363a1b35..0260c70309 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 @@ -22,6 +22,8 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable; +import com.scalar.db.api.TransactionManagerCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; @@ -38,6 +40,8 @@ import com.scalar.db.transaction.consensuscommit.Coordinator.State; import com.scalar.db.transaction.consensuscommit.CoordinatorGroupCommitter.CoordinatorGroupCommitKeyManipulator; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -666,6 +670,338 @@ public void scan_ShouldScan() throws TransactionException { assertThat(actual).isEqualTo(results); } + @Test + public void getScannerAndScannerOne_ShouldReturnScannerAndReturnProperResult() throws Exception { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThat(actual.one()).hasValue(result1); + assertThat(actual.one()).hasValue(result2); + assertThat(actual.one()).hasValue(result3); + assertThat(actual.one()).isEmpty(); + actual.close(); + + verify(spied).begin(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerAll_ShouldReturnScannerAndReturnProperResults() throws Exception { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.all()) + .thenReturn(Arrays.asList(result1, result2, result3)) + .thenReturn(Collections.emptyList()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + List results = actual.all(); + assertThat(results).containsExactly(result1, result2, result3); + assertThat(actual.all()).isEmpty(); + actual.close(); + + verify(spied).begin(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerIterator_ShouldReturnScannerAndReturnProperResults() + throws Exception { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + + Iterator iterator = actual.iterator(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result1); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result2); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result3); + assertThat(iterator.hasNext()).isFalse(); + actual.close(); + + verify(spied).begin(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void + getScanner_CrudExceptionThrownByTransactionGetScanner_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + when(transaction.getScanner(scan)).thenThrow(CrudException.class); + + // Act Assert + assertThatThrownBy(() -> spied.getScanner(scan)).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerOne_CrudExceptionThrownByScannerOne_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + when(scanner.one()).thenThrow(CrudException.class); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::one).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerAll_CrudExceptionThrownByScannerAll_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + when(scanner.all()).thenThrow(CrudException.class); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::all).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_CrudExceptionThrownByScannerClose_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + doThrow(CrudException.class).when(scanner).close(); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_CommitConflictExceptionThrownByTransactionCommit_ShouldRollbackTransactionAndThrowCrudConflictException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(CommitConflictException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudConflictException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_UnknownTransactionStatusExceptionByTransactionCommit_ShouldThrowUnknownTransactionStatusException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(UnknownTransactionStatusException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(UnknownTransactionStatusException.class); + + verify(spied).begin(); + verify(scanner).close(); + } + + @Test + public void + getScannerAndScannerClose_CommitExceptionThrownByTransactionCommit_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + ConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(CommitException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + @Test public void put_ShouldPut() throws TransactionException { // Arrange 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 536d2ebfe5..f81a85b70b 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 @@ -21,6 +21,7 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; import com.scalar.db.exception.storage.ExecutionException; @@ -68,6 +69,9 @@ public class ConsensusCommitTest { @BeforeEach public void setUp() throws Exception { MockitoAnnotations.openMocks(this).close(); + + // Arrange + when(crud.areAllScannersClosed()).thenReturn(true); } private Get prepareGet() { @@ -164,6 +168,93 @@ public void scan_ScanForUncommittedRecordGiven_ShouldRecoverRecord() throws Crud verify(recovery).recover(scan, result); } + @Test + public void getScannerAndScannerOne_ShouldCallCrudHandlerGetScannerAndScannerOne() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + Result result = mock(Result.class); + when(scanner.one()).thenReturn(Optional.of(result)); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act + TransactionCrudOperable.Scanner actualScanner = consensus.getScanner(scan); + Optional actualResult = actualScanner.one(); + + // Assert + assertThat(actualResult).hasValue(result); + verify(crud).getScanner(scan); + verify(scanner).one(); + } + + @Test + public void + getScannerAndScannerOne_UncommittedRecordExceptionThrownByScannerOne_ShouldRecoverRecord() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + + UncommittedRecordException toThrow = mock(UncommittedRecordException.class); + TransactionResult result = mock(TransactionResult.class); + when(toThrow.getSelection()).thenReturn(scan); + when(toThrow.getResults()).thenReturn(Collections.singletonList(result)); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(scanner.one()).thenThrow(toThrow); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actualScanner = consensus.getScanner(scan); + assertThatThrownBy(actualScanner::one).isInstanceOf(UncommittedRecordException.class); + + verify(recovery).recover(scan, result); + } + + @Test + public void getScannerAndScannerAll_ShouldCallCrudHandlerGetScannerAndScannerAll() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + when(scanner.all()).thenReturn(Arrays.asList(result1, result2)); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act + TransactionCrudOperable.Scanner actualScanner = consensus.getScanner(scan); + List actualResults = actualScanner.all(); + + // Assert + assertThat(actualResults).containsExactly(result1, result2); + verify(crud).getScanner(scan); + verify(scanner).all(); + } + + @Test + public void + getScannerAndScannerAll_UncommittedRecordExceptionThrownByScannerAll_ShouldRecoverRecord() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + + UncommittedRecordException toThrow = mock(UncommittedRecordException.class); + TransactionResult result = mock(TransactionResult.class); + when(toThrow.getSelection()).thenReturn(scan); + when(toThrow.getResults()).thenReturn(Collections.singletonList(result)); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(scanner.all()).thenThrow(toThrow); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actualScanner = consensus.getScanner(scan); + assertThatThrownBy(actualScanner::all).isInstanceOf(UncommittedRecordException.class); + + verify(recovery).recover(scan, result); + } + @Test public void put_PutGiven_ShouldCallCrudHandlerPut() throws ExecutionException, CrudException { // Arrange @@ -674,21 +765,30 @@ public void commit_ProcessedCrudGiven_ShouldCommitWithSnapshot() } @Test - public void rollback_WithoutGroupCommitter_ShouldDoNothing() - throws UnknownTransactionStatusException { + public void commit_ScannerNotClosed_ShouldThrowIllegalStateException() { + // Arrange + when(crud.areAllScannersClosed()).thenReturn(false); + + // Act Assert + assertThatThrownBy(() -> consensus.commit()).isInstanceOf(IllegalStateException.class); + } + + @Test + public void rollback_ShouldDoNothing() throws CrudException, UnknownTransactionStatusException { // Arrange // Act consensus.rollback(); // Assert + verify(crud).closeScanners(); verify(commit, never()).rollbackRecords(any(Snapshot.class)); verify(commit, never()).abortState(anyString()); } @Test public void rollback_WithGroupCommitter_ShouldRemoveTxFromGroupCommitter() - throws UnknownTransactionStatusException { + throws CrudException, UnknownTransactionStatusException { // Arrange String txId = "tx-id"; Snapshot snapshot = mock(Snapshot.class); @@ -702,6 +802,7 @@ public void rollback_WithGroupCommitter_ShouldRemoveTxFromGroupCommitter() consensusWithGroupCommit.rollback(); // Assert + verify(crud).closeScanners(); verify(groupCommitter).remove(txId); verify(commit, never()).rollbackRecords(any(Snapshot.class)); verify(commit, never()).abortState(anyString()); 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 933d40b5d6..689ede3bb9 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 @@ -24,6 +24,7 @@ import com.scalar.db.api.ScanAll; import com.scalar.db.api.Scanner; import com.scalar.db.api.TableMetadata; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.common.ResultImpl; import com.scalar.db.exception.storage.ExecutionException; @@ -33,6 +34,8 @@ import com.scalar.db.io.Key; import com.scalar.db.io.TextColumn; import com.scalar.db.util.ScalarDbUtils; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -44,8 +47,9 @@ import java.util.concurrent.ConcurrentMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -231,8 +235,8 @@ public void get_GetExistsInSnapshot_ShouldReturnFromSnapshot() throws CrudExcept assertThat(exception.getResults().get(0)).isEqualTo(result); }); - verify(snapshot, never()).putIntoReadSet(any(), ArgumentMatchers.any()); - verify(snapshot, never()).putIntoGetSet(any(), ArgumentMatchers.any()); + verify(snapshot, never()).putIntoReadSet(any(), any()); + verify(snapshot, never()).putIntoGetSet(any(), any()); } @Test @@ -345,23 +349,29 @@ public void get_ForNonExistingTable_ShouldThrowIllegalArgumentException() assertThatThrownBy(() -> handler.get(get)).isInstanceOf(IllegalArgumentException.class); } - @Test - public void scan_ResultGivenFromStorage_ShouldUpdateSnapshotAndReturn() - throws ExecutionException, CrudException { + @ParameterizedTest + @EnumSource(ScanType.class) + void scanOrGetScanner_ResultGivenFromStorage_ShouldUpdateSnapshotAndReturn(ScanType scanType) + throws ExecutionException, CrudException, IOException { // Arrange Scan scan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(scan); result = prepareResult(TransactionState.COMMITTED); Snapshot.Key key = new Snapshot.Key(scan, result); TransactionResult expected = new TransactionResult(result); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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); when(snapshot.getResult(any())).thenReturn(Optional.of(expected)); // Act - List results = handler.scan(scan); + List results = scanOrGetScanner(scan, scanType); // Assert + verify(scanner).close(); verify(snapshot).putIntoReadSet(key, Optional.of(expected)); verify(snapshot).putIntoScanSet(scan, Maps.newLinkedHashMap(ImmutableMap.of(key, expected))); verify(snapshot).verifyNoOverlap(scan, ImmutableMap.of(key, expected)); @@ -370,19 +380,24 @@ public void scan_ResultGivenFromStorage_ShouldUpdateSnapshotAndReturn() .isEqualTo(new FilteredResult(expected, Collections.emptyList(), TABLE_METADATA, false)); } - @Test - public void - scan_PreparedResultGivenFromStorage_ShouldNeverUpdateSnapshotThrowUncommittedRecordException() - throws ExecutionException { + @ParameterizedTest + @EnumSource(ScanType.class) + void + scanOrGetScanner_PreparedResultGivenFromStorage_ShouldNeverUpdateSnapshotThrowUncommittedRecordException( + ScanType scanType) throws ExecutionException, IOException { // Arrange Scan scan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(scan); result = prepareResult(TransactionState.PREPARED); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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); // Act Assert - assertThatThrownBy(() -> handler.scan(scan)) + assertThatThrownBy(() -> scanOrGetScanner(scan, scanType)) .isInstanceOf(UncommittedRecordException.class) .satisfies( e -> { @@ -392,13 +407,15 @@ public void scan_ResultGivenFromStorage_ShouldUpdateSnapshotAndReturn() assertThat(exception.getResults().get(0)).isEqualTo(result); }); - verify(snapshot, never()).putIntoReadSet(any(), ArgumentMatchers.any()); - verify(snapshot, never()).putIntoScanSet(any(), ArgumentMatchers.any()); + verify(scanner).close(); + verify(snapshot, never()).putIntoReadSet(any(), any()); + verify(snapshot, never()).putIntoScanSet(any(), any()); } - @Test - public void scan_CalledTwice_SecondTimeShouldReturnTheSameFromSnapshot() - throws ExecutionException, CrudException { + @ParameterizedTest + @EnumSource(ScanType.class) + void scanOrGetScanner_CalledTwice_SecondTimeShouldReturnTheSameFromSnapshot(ScanType scanType) + throws ExecutionException, CrudException, IOException { // Arrange Scan originalScan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(originalScan); @@ -406,7 +423,11 @@ public void scan_CalledTwice_SecondTimeShouldReturnTheSameFromSnapshot() Scan scan2 = prepareScan(); result = prepareResult(TransactionState.COMMITTED); TransactionResult expected = new TransactionResult(result); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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(scanForStorage, result); when(snapshot.getResults(scanForStorage)) @@ -415,10 +436,11 @@ public void scan_CalledTwice_SecondTimeShouldReturnTheSameFromSnapshot() when(snapshot.getResult(key)).thenReturn(Optional.of(expected)); // Act - List results1 = handler.scan(scan1); - List results2 = handler.scan(scan2); + List results1 = scanOrGetScanner(scan1, scanType); + List results2 = scanOrGetScanner(scan2, scanType); // Assert + verify(scanner).close(); verify(snapshot).putIntoReadSet(key, Optional.of(expected)); verify(snapshot) .putIntoScanSet(scanForStorage, Maps.newLinkedHashMap(ImmutableMap.of(key, expected))); @@ -430,9 +452,10 @@ public void scan_CalledTwice_SecondTimeShouldReturnTheSameFromSnapshot() verify(storage).scan(scanForStorage); } - @Test - public void scan_CalledTwiceUnderRealSnapshot_SecondTimeShouldReturnTheSameFromSnapshot() - throws ExecutionException, CrudException { + @ParameterizedTest + @EnumSource(ScanType.class) + void scan_CalledTwiceUnderRealSnapshot_SecondTimeShouldReturnTheSameFromSnapshot( + ScanType scanType) throws ExecutionException, CrudException, IOException { // Arrange Scan originalScan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(originalScan); @@ -442,30 +465,41 @@ public void scan_CalledTwiceUnderRealSnapshot_SecondTimeShouldReturnTheSameFromS TransactionResult expected = new TransactionResult(result); snapshot = new Snapshot(ANY_TX_ID, Isolation.SNAPSHOT, tableMetadataManager, parallelExecutor); handler = new CrudHandler(storage, snapshot, tableMetadataManager, false, parallelExecutor); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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); // Act - List results1 = handler.scan(scan1); - List results2 = handler.scan(scan2); + List results1 = scanOrGetScanner(scan1, scanType); + List results2 = scanOrGetScanner(scan2, scanType); // Assert assertThat(results1.size()).isEqualTo(1); assertThat(results1.get(0)) .isEqualTo(new FilteredResult(expected, Collections.emptyList(), TABLE_METADATA, false)); assertThat(results1).isEqualTo(results2); + + verify(scanner).close(); verify(storage, never()).scan(originalScan); verify(storage).scan(scanForStorage); } - @Test - public void scan_GetCalledAfterScan_ShouldReturnFromStorage() - throws ExecutionException, CrudException { + @ParameterizedTest + @EnumSource(ScanType.class) + void scanOrGetScanner_GetCalledAfterScan_ShouldReturnFromStorage(ScanType scanType) + throws ExecutionException, CrudException, IOException { // Arrange Scan scan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(scan); result = prepareResult(TransactionState.COMMITTED); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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); Get get = prepareGet(); Snapshot.Key key = new Snapshot.Key(get); @@ -476,46 +510,55 @@ public void scan_GetCalledAfterScan_ShouldReturnFromStorage() when(snapshot.getResult(key)).thenReturn(transactionResult); // Act - List results = handler.scan(scan); + List results = scanOrGetScanner(scan, scanType); Optional result = handler.get(get); // Assert verify(storage).scan(scanForStorage); - verify(storage).get(getForStorage); + verify(scanner).close(); + assertThat(results.size()).isEqualTo(1); assertThat(result).isPresent(); assertThat(results.get(0)).isEqualTo(result.get()); } - @Test - public void scan_GetCalledAfterScanUnderRealSnapshot_ShouldReturnFromStorage() - throws ExecutionException, CrudException { + @ParameterizedTest + @EnumSource(ScanType.class) + void scanOrGetScanner_GetCalledAfterScanUnderRealSnapshot_ShouldReturnFromStorage( + ScanType scanType) throws ExecutionException, CrudException, IOException { // Arrange Scan scan = toScanForStorageFrom(prepareScan()); result = prepareResult(TransactionState.COMMITTED); snapshot = new Snapshot(ANY_TX_ID, Isolation.SNAPSHOT, tableMetadataManager, parallelExecutor); handler = new CrudHandler(storage, snapshot, tableMetadataManager, false, parallelExecutor); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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(scan)).thenReturn(scanner); Get get = prepareGet(); when(storage.get(get)).thenReturn(Optional.of(result)); // Act - List results = handler.scan(scan); + List results = scanOrGetScanner(scan, scanType); Optional result = handler.get(get); // Assert verify(storage).scan(scan); verify(storage).get(get); + verify(scanner).close(); + assertThat(results.size()).isEqualTo(1); assertThat(result).isPresent(); assertThat(results.get(0)).isEqualTo(result.get()); } - @Test - public void scan_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgumentException() - throws ExecutionException, CrudException { + @ParameterizedTest + @EnumSource(ScanType.class) + void scanOrGetScanner_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgumentException( + ScanType scanType) throws ExecutionException, CrudException, IOException { // Arrange Scan scan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(scan); @@ -551,9 +594,17 @@ public void scan_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgumentEx new ConcurrentHashMap<>(), new HashMap<>(), new HashMap<>(), - deleteSet); + deleteSet, + new ArrayList<>()); handler = new CrudHandler(storage, snapshot, tableMetadataManager, false, parallelExecutor); - when(scanner.iterator()).thenReturn(Arrays.asList(result, result2).iterator()); + if (scanType == ScanType.SCAN) { + when(scanner.iterator()).thenReturn(Arrays.asList(result, result2).iterator()); + } else { + when(scanner.one()) + .thenReturn(Optional.of(result)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.empty()); + } when(storage.scan(scanForStorage)).thenReturn(scanner); Delete delete = @@ -568,26 +619,35 @@ public void scan_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgumentEx assertThat(deleteSet.size()).isEqualTo(1); assertThat(deleteSet).containsKey(new Snapshot.Key(delete)); - assertThatThrownBy(() -> handler.scan(scan)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> scanOrGetScanner(scan, scanType)) + .isInstanceOf(IllegalArgumentException.class); + + verify(scanner).close(); } - @Test - public void - scan_CrossPartitionScanAndResultFromStorageGiven_ShouldUpdateSnapshotAndVerifyNoOverlapThenReturn() - throws ExecutionException, CrudException { + @ParameterizedTest + @EnumSource(ScanType.class) + void + scanOrGetScanner_CrossPartitionScanAndResultFromStorageGiven_ShouldUpdateSnapshotAndVerifyNoOverlapThenReturn( + ScanType scanType) throws ExecutionException, CrudException, IOException { // Arrange Scan scan = prepareCrossPartitionScan(); result = prepareResult(TransactionState.COMMITTED); Snapshot.Key key = new Snapshot.Key(scan, result); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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(any(ScanAll.class))).thenReturn(scanner); TransactionResult transactionResult = new TransactionResult(result); when(snapshot.getResult(key)).thenReturn(Optional.of(transactionResult)); // Act - List results = handler.scan(scan); + List results = scanOrGetScanner(scan, scanType); // Assert + verify(scanner).close(); verify(snapshot).putIntoReadSet(key, Optional.of(transactionResult)); verify(snapshot) .putIntoScanSet(scan, Maps.newLinkedHashMap(ImmutableMap.of(key, transactionResult))); @@ -598,18 +658,23 @@ public void scan_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgumentEx new FilteredResult(transactionResult, Collections.emptyList(), TABLE_METADATA, false)); } - @Test - public void - scan_CrossPartitionScanAndPreparedResultFromStorageGiven_ShouldNeverUpdateSnapshotNorVerifyNoOverlapButThrowUncommittedRecordException() - throws ExecutionException { + @ParameterizedTest + @EnumSource(ScanType.class) + void + scanOrGetScanner_CrossPartitionScanAndPreparedResultFromStorageGiven_ShouldNeverUpdateSnapshotNorVerifyNoOverlapButThrowUncommittedRecordException( + ScanType scanType) throws ExecutionException, IOException { // Arrange Scan scan = prepareCrossPartitionScan(); result = prepareResult(TransactionState.PREPARED); - when(scanner.iterator()).thenReturn(Collections.singletonList(result).iterator()); + 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(any(ScanAll.class))).thenReturn(scanner); // Act Assert - assertThatThrownBy(() -> handler.scan(scan)) + assertThatThrownBy(() -> scanOrGetScanner(scan, scanType)) .isInstanceOf(UncommittedRecordException.class) .satisfies( e -> { @@ -619,14 +684,16 @@ public void scan_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgumentEx assertThat(exception.getResults().get(0)).isEqualTo(result); }); - verify(snapshot, never()).putIntoReadSet(any(Snapshot.Key.class), ArgumentMatchers.any()); + verify(scanner).close(); + verify(snapshot, never()).putIntoReadSet(any(Snapshot.Key.class), any()); + verify(snapshot, never()).putIntoScannerSet(any(Scan.class), any()); verify(snapshot, never()).verifyNoOverlap(any(), any()); } @Test public void scan_RuntimeExceptionCausedByExecutionExceptionThrownByIteratorHasNext_ShouldThrowCrudException() - throws ExecutionException { + throws ExecutionException, IOException { // Arrange Scan scan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(scan); @@ -643,11 +710,13 @@ public void scan_CalledAfterDeleteUnderRealSnapshot_ShouldThrowIllegalArgumentEx assertThatThrownBy(() -> handler.scan(scan)) .isInstanceOf(CrudException.class) .hasCause(executionException); + + verify(scanner).close(); } @Test public void scan_RuntimeExceptionThrownByIteratorHasNext_ShouldThrowCrudException() - throws ExecutionException { + throws ExecutionException, IOException { // Arrange Scan scan = prepareScan(); Scan scanForStorage = toScanForStorageFrom(scan); @@ -662,6 +731,60 @@ public void scan_RuntimeExceptionThrownByIteratorHasNext_ShouldThrowCrudExceptio assertThatThrownBy(() -> handler.scan(scan)) .isInstanceOf(CrudException.class) .hasCause(runtimeException); + + verify(scanner).close(); + } + + @Test + public void getScanner_ExecutionExceptionThrownByScannerOne_ShouldThrowCrudException() + throws ExecutionException, IOException, CrudException { + // Arrange + Scan scan = prepareScan(); + Scan scanForStorage = toScanForStorageFrom(scan); + ExecutionException executionException = mock(ExecutionException.class); + when(scanner.one()).thenThrow(executionException); + when(storage.scan(scanForStorage)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actualScanner = handler.getScanner(scan); + assertThatThrownBy(actualScanner::one) + .isInstanceOf(CrudException.class) + .hasCause(executionException); + + verify(scanner).close(); + } + + @Test + public void + getScanner_ScannerNotFullyScanned_ShouldPutReadSetAndScannerSetInSnapshotAndVerifyScan() + throws ExecutionException, CrudException, IOException { + // Arrange + Scan scan = prepareScan(); + Scan scanForStorage = toScanForStorageFrom(scan); + Result result1 = prepareResult(TransactionState.COMMITTED); + Result result2 = prepareResult(TransactionState.COMMITTED); + Snapshot.Key key1 = new Snapshot.Key(scan, result1); + TransactionResult txResult1 = new TransactionResult(result1); + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.empty()); + when(storage.scan(scanForStorage)).thenReturn(scanner); + + // Act + TransactionCrudOperable.Scanner actualScanner = handler.getScanner(scan); + Optional actualResult = actualScanner.one(); + actualScanner.close(); + + // Assert + verify(scanner).close(); + verify(snapshot).putIntoReadSet(key1, Optional.of(txResult1)); + verify(snapshot) + .putIntoScannerSet(scan, Maps.newLinkedHashMap(ImmutableMap.of(key1, txResult1))); + verify(snapshot).verifyNoOverlap(scan, ImmutableMap.of(key1, txResult1)); + + assertThat(actualResult) + .hasValue(new FilteredResult(txResult1, Collections.emptyList(), TABLE_METADATA, false)); } @Test @@ -1191,4 +1314,34 @@ public void readIfImplicitPreReadEnabled_ShouldCallAppropriateMethods() throws C assertThat(transactionIdCaptor.getValue()).isEqualTo(ANY_TX_ID); } + + private List scanOrGetScanner(Scan scan, ScanType scanType) throws CrudException { + if (scanType == ScanType.SCAN) { + return handler.scan(scan); + } + + try (TransactionCrudOperable.Scanner scanner = handler.getScanner(scan)) { + switch (scanType) { + case SCANNER_ONE: + List results = new ArrayList<>(); + while (true) { + Optional result = scanner.one(); + if (!result.isPresent()) { + return results; + } + results.add(result.get()); + } + case SCANNER_ALL: + return scanner.all(); + default: + throw new AssertionError(); + } + } + } + + enum ScanType { + SCAN, + SCANNER_ONE, + SCANNER_ALL + } } diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java index 019df03c64..06b5d0aa1c 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/SnapshotTest.java @@ -41,12 +41,14 @@ import com.scalar.db.io.Value; import com.scalar.db.transaction.consensuscommit.Snapshot.ReadWriteSets; import com.scalar.db.util.ScalarDbUtils; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -98,6 +100,7 @@ public class SnapshotTest { private Map> scanSet; private Map writeSet; private Map deleteSet; + private List scannerSet; @Mock private ConsensusCommitConfig config; @Mock private PrepareMutationComposer prepareComposer; @@ -122,6 +125,7 @@ private Snapshot prepareSnapshot(Isolation isolation) { scanSet = new HashMap<>(); writeSet = new HashMap<>(); deleteSet = new HashMap<>(); + scannerSet = new ArrayList<>(); return spy( new Snapshot( @@ -133,7 +137,8 @@ private Snapshot prepareSnapshot(Isolation isolation) { getSet, scanSet, writeSet, - deleteSet)); + deleteSet, + scannerSet)); } private TransactionResult prepareResult(String txId) { @@ -1614,6 +1619,33 @@ public void toSerializable_ScanWithLimitInScanSet_ShouldProcessWithoutExceptions verify(storage).scan(scanWithProjectionsWithoutLimit); } + @Test + public void toSerializable_ScannerSetNotChanged_ShouldProcessWithoutExceptions() + throws ExecutionException { + // Arrange + snapshot = prepareSnapshot(Isolation.SERIALIZABLE); + Scan scan = prepareScan(); + TransactionResult result1 = prepareResult(ANY_ID + "x", ANY_TEXT_1, ANY_TEXT_2); + TransactionResult result2 = prepareResult(ANY_ID + "x", ANY_TEXT_1, ANY_TEXT_3); + Snapshot.Key key1 = new Snapshot.Key(scan, result1); + snapshot.putIntoScannerSet(scan, Maps.newLinkedHashMap(ImmutableMap.of(key1, result1))); + DistributedStorage storage = mock(DistributedStorage.class); + Scan scanWithProjections = + Scan.newBuilder(scan).projections(Attribute.ID, ANY_NAME_1, ANY_NAME_2).build(); + Scanner scanner = mock(Scanner.class); + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.empty()); + when(storage.scan(scanWithProjections)).thenReturn(scanner); + + // Act Assert + assertThatCode(() -> snapshot.toSerializable(storage)).doesNotThrowAnyException(); + + // Assert + verify(storage).scan(scanWithProjections); + } + @Test public void verifyNoOverlap_ScanGivenAndDeleteKeyAlreadyPresentInDeleteSet_ShouldThrowIllegalArgumentException() { diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java index ba6c29ad56..880e7466c6 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TwoPhaseConsensusCommitManagerTest.java @@ -19,6 +19,8 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable; +import com.scalar.db.api.TransactionManagerCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.TwoPhaseCommitTransaction; import com.scalar.db.api.TwoPhaseCommitTransactionManager; @@ -40,6 +42,8 @@ import com.scalar.db.io.Key; import com.scalar.db.transaction.consensuscommit.Coordinator.State; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -702,6 +706,404 @@ public void scan_ShouldScan() throws TransactionException { assertThat(actual).isEqualTo(results); } + @Test + public void getScannerAndScannerOne_ShouldReturnScannerAndReturnProperResult() throws Exception { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThat(actual.one()).hasValue(result1); + assertThat(actual.one()).hasValue(result2); + assertThat(actual.one()).hasValue(result3); + assertThat(actual.one()).isEmpty(); + actual.close(); + + verify(spied).begin(); + verify(transaction).prepare(); + verify(transaction).validate(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerAll_ShouldReturnScannerAndReturnProperResults() throws Exception { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.all()) + .thenReturn(Arrays.asList(result1, result2, result3)) + .thenReturn(Collections.emptyList()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + List results = actual.all(); + assertThat(results).containsExactly(result1, result2, result3); + assertThat(actual.all()).isEmpty(); + actual.close(); + + verify(spied).begin(); + verify(transaction).prepare(); + verify(transaction).validate(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerIterator_ShouldReturnScannerAndReturnProperResults() + throws Exception { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + + Iterator iterator = actual.iterator(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result1); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result2); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result3); + assertThat(iterator.hasNext()).isFalse(); + actual.close(); + + verify(spied).begin(); + verify(transaction).prepare(); + verify(transaction).validate(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void + getScanner_CrudExceptionThrownByTransactionGetScanner_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + when(transaction.getScanner(scan)).thenThrow(CrudException.class); + + // Act Assert + assertThatThrownBy(() -> spied.getScanner(scan)).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerOne_CrudExceptionThrownByScannerOne_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + when(scanner.one()).thenThrow(CrudException.class); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::one).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerAll_CrudExceptionThrownByScannerAll_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + when(scanner.all()).thenThrow(CrudException.class); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::all).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_CrudExceptionThrownByScannerClose_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + doThrow(CrudException.class).when(scanner).close(); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_PreparationConflictExceptionThrownByTransactionPrepare_ShouldRollbackTransactionAndThrowCrudConflictException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(PreparationConflictException.class).when(transaction).prepare(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudConflictException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_ValidationConflictExceptionThrownByTransactionValidate_ShouldRollbackTransactionAndThrowCrudConflictException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(ValidationConflictException.class).when(transaction).validate(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudConflictException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_CommitConflictExceptionThrownByTransactionCommit_ShouldRollbackTransactionAndThrowCrudConflictException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(CommitConflictException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudConflictException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_UnknownTransactionStatusExceptionByTransactionCommit_ShouldThrowUnknownTransactionStatusException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(UnknownTransactionStatusException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(UnknownTransactionStatusException.class); + + verify(spied).begin(); + verify(scanner).close(); + } + + @Test + public void + getScannerAndScannerClose_CommitExceptionThrownByTransactionCommit_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + TwoPhaseCommitTransaction transaction = mock(TwoPhaseCommitTransaction.class); + + TwoPhaseConsensusCommitManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(CommitException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace("ns") + .table("tbl") + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + @Test public void put_ShouldPut() throws TransactionException { // Arrange 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 bd12bbea43..0b80bf8b22 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 @@ -17,6 +17,7 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; @@ -28,6 +29,7 @@ import com.scalar.db.exception.transaction.PreparationConflictException; import com.scalar.db.exception.transaction.PreparationException; import com.scalar.db.exception.transaction.RollbackException; +import com.scalar.db.exception.transaction.TransactionException; import com.scalar.db.exception.transaction.UnknownTransactionStatusException; import com.scalar.db.exception.transaction.UnsatisfiedConditionException; import com.scalar.db.exception.transaction.ValidationException; @@ -65,7 +67,10 @@ public class TwoPhaseConsensusCommitTest { public void setUp() throws Exception { MockitoAnnotations.openMocks(this).close(); + // Arrange transaction = new TwoPhaseConsensusCommit(crud, commit, recovery, mutationOperationChecker); + + when(crud.areAllScannersClosed()).thenReturn(true); } private Get prepareGet() { @@ -166,6 +171,93 @@ public void scan_ScanForUncommittedRecordGiven_ShouldRecoverRecord() throws Crud verify(recovery).recover(scan, result); } + @Test + public void getScannerAndScannerOne_ShouldCallCrudHandlerGetScannerAndScannerOne() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + Result result = mock(Result.class); + when(scanner.one()).thenReturn(Optional.of(result)); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act + TransactionCrudOperable.Scanner actualScanner = transaction.getScanner(scan); + Optional actualResult = actualScanner.one(); + + // Assert + assertThat(actualResult).hasValue(result); + verify(crud).getScanner(scan); + verify(scanner).one(); + } + + @Test + public void + getScannerAndScannerOne_UncommittedRecordExceptionThrownByScannerOne_ShouldRecoverRecord() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + + UncommittedRecordException toThrow = mock(UncommittedRecordException.class); + TransactionResult result = mock(TransactionResult.class); + when(toThrow.getSelection()).thenReturn(scan); + when(toThrow.getResults()).thenReturn(Collections.singletonList(result)); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(scanner.one()).thenThrow(toThrow); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actualScanner = transaction.getScanner(scan); + assertThatThrownBy(actualScanner::one).isInstanceOf(UncommittedRecordException.class); + + verify(recovery).recover(scan, result); + } + + @Test + public void getScannerAndScannerAll_ShouldCallCrudHandlerGetScannerAndScannerAll() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + when(scanner.all()).thenReturn(Arrays.asList(result1, result2)); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act + TransactionCrudOperable.Scanner actualScanner = transaction.getScanner(scan); + List actualResults = actualScanner.all(); + + // Assert + assertThat(actualResults).containsExactly(result1, result2); + verify(crud).getScanner(scan); + verify(scanner).all(); + } + + @Test + public void + getScannerAndScannerAll_UncommittedRecordExceptionThrownByScannerAll_ShouldRecoverRecord() + throws CrudException { + // Arrange + Scan scan = prepareScan(); + + UncommittedRecordException toThrow = mock(UncommittedRecordException.class); + TransactionResult result = mock(TransactionResult.class); + when(toThrow.getSelection()).thenReturn(scan); + when(toThrow.getResults()).thenReturn(Collections.singletonList(result)); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(scanner.all()).thenThrow(toThrow); + when(crud.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actualScanner = transaction.getScanner(scan); + assertThatThrownBy(actualScanner::all).isInstanceOf(UncommittedRecordException.class); + + verify(recovery).recover(scan, result); + } + @Test public void put_PutGiven_ShouldCallCrudHandlerPut() throws ExecutionException, CrudException { // Arrange @@ -673,6 +765,15 @@ public void prepare_ProcessedCrudGiven_ShouldPrepareWithSnapshot() assertThatThrownBy(transaction::prepare).isInstanceOf(PreparationException.class); } + @Test + public void prepare_ScannerNotClosed_ShouldThrowIllegalStateException() { + // Arrange + when(crud.areAllScannersClosed()).thenReturn(false); + + // Act Assert + assertThatThrownBy(() -> transaction.prepare()).isInstanceOf(IllegalStateException.class); + } + @Test public void validate_ProcessedCrudGiven_ShouldPerformValidationWithSnapshot() throws ValidationException, PreparationException { @@ -735,8 +836,7 @@ public void commit_SerializableUsedAndPreparedState_ShouldThrowIllegalStateExcep } @Test - public void rollback_ShouldAbortStateAndRollbackRecords() - throws RollbackException, UnknownTransactionStatusException, PreparationException { + public void rollback_ShouldAbortStateAndRollbackRecords() throws TransactionException { // Arrange transaction.prepare(); when(crud.getSnapshot()).thenReturn(snapshot); @@ -745,13 +845,14 @@ public void rollback_ShouldAbortStateAndRollbackRecords() transaction.rollback(); // Assert + verify(crud).closeScanners(); verify(commit).abortState(snapshot.getId()); verify(commit).rollbackRecords(snapshot); } @Test public void rollback_CalledAfterPrepareFails_ShouldAbortStateAndRollbackRecords() - throws PreparationException, UnknownTransactionStatusException, RollbackException { + throws TransactionException { // Arrange when(crud.getSnapshot()).thenReturn(snapshot); doThrow(PreparationException.class).when(commit).prepare(snapshot); @@ -761,14 +862,14 @@ public void rollback_CalledAfterPrepareFails_ShouldAbortStateAndRollbackRecords( transaction.rollback(); // Assert + verify(crud).closeScanners(); verify(commit).abortState(snapshot.getId()); verify(commit).rollbackRecords(snapshot); } @Test public void rollback_CalledAfterCommitFails_ShouldNeverAbortStateAndRollbackRecords() - throws CommitException, UnknownTransactionStatusException, RollbackException, - PreparationException { + throws TransactionException { // Arrange transaction.prepare(); when(crud.getSnapshot()).thenReturn(snapshot); @@ -779,6 +880,7 @@ public void rollback_CalledAfterCommitFails_ShouldNeverAbortStateAndRollbackReco transaction.rollback(); // Assert + verify(crud).closeScanners(); verify(commit, never()).abortState(snapshot.getId()); verify(commit, never()).rollbackRecords(snapshot); } @@ -786,7 +888,7 @@ public void rollback_CalledAfterCommitFails_ShouldNeverAbortStateAndRollbackReco @Test public void rollback_UnknownTransactionStatusExceptionThrownByAbortState_ShouldThrowRollbackException() - throws UnknownTransactionStatusException, PreparationException { + throws TransactionException { // Arrange transaction.prepare(); when(crud.getSnapshot()).thenReturn(snapshot); @@ -795,12 +897,13 @@ public void rollback_CalledAfterCommitFails_ShouldNeverAbortStateAndRollbackReco // Act Assert assertThatThrownBy(transaction::rollback).isInstanceOf(RollbackException.class); + verify(crud).closeScanners(); verify(commit, never()).rollbackRecords(snapshot); } @Test public void rollback_CommittedStateReturnedByAbortState_ShouldThrowRollbackException() - throws UnknownTransactionStatusException, PreparationException { + throws TransactionException { // Arrange transaction.prepare(); when(crud.getSnapshot()).thenReturn(snapshot); @@ -809,6 +912,7 @@ public void rollback_CommittedStateReturnedByAbortState_ShouldThrowRollbackExcep // Act Assert assertThatThrownBy(transaction::rollback).isInstanceOf(RollbackException.class); + verify(crud).closeScanners(); verify(commit, never()).rollbackRecords(snapshot); } } diff --git a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java index 813d757334..80d0375e6d 100644 --- a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionManagerTest.java @@ -20,6 +20,8 @@ import com.scalar.db.api.Put; import com.scalar.db.api.Result; import com.scalar.db.api.Scan; +import com.scalar.db.api.TransactionCrudOperable; +import com.scalar.db.api.TransactionManagerCrudOperable; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; import com.scalar.db.common.ActiveTransactionManagedDistributedTransactionManager; @@ -41,6 +43,7 @@ import java.sql.SQLException; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Optional; import org.apache.commons.dbcp2.BasicDataSource; @@ -178,6 +181,369 @@ public void scan_withConflictError_shouldThrowCrudConflictException() .isInstanceOf(CrudConflictException.class); } + @Test + public void getScannerAndScannerOne_ShouldReturnScannerAndReturnProperResult() throws Exception { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThat(actual.one()).hasValue(result1); + assertThat(actual.one()).hasValue(result2); + assertThat(actual.one()).hasValue(result3); + assertThat(actual.one()).isEmpty(); + actual.close(); + + verify(spied).begin(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerAll_ShouldReturnScannerAndReturnProperResults() throws Exception { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.all()) + .thenReturn(Arrays.asList(result1, result2, result3)) + .thenReturn(Collections.emptyList()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + List results = actual.all(); + assertThat(results).containsExactly(result1, result2, result3); + assertThat(actual.all()).isEmpty(); + actual.close(); + + verify(spied).begin(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerIterator_ShouldReturnScannerAndReturnProperResults() + throws Exception { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + + Iterator iterator = actual.iterator(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result1); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result2); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result3); + assertThat(iterator.hasNext()).isFalse(); + actual.close(); + + verify(spied).begin(); + verify(transaction).commit(); + verify(scanner).close(); + } + + @Test + public void + getScanner_TransactionNotFoundExceptionThrownByTransactionBegin_ShouldThrowCrudConflictException() + throws TransactionException { + // Arrange + JdbcTransactionManager spied = spy(manager); + doThrow(TransactionNotFoundException.class).when(spied).begin(); + + Scan scan = mock(Scan.class); + + // Act Assert + assertThatThrownBy(() -> spied.getScanner(scan)).isInstanceOf(CrudConflictException.class); + + verify(spied).begin(); + } + + @Test + public void getScanner_TransactionExceptionThrownByTransactionBegin_ShouldThrowCrudException() + throws TransactionException { + // Arrange + JdbcTransactionManager spied = spy(manager); + doThrow(TransactionException.class).when(spied).begin(); + + Scan scan = mock(Scan.class); + + // Act Assert + assertThatThrownBy(() -> spied.getScanner(scan)).isInstanceOf(CrudException.class); + + verify(spied).begin(); + } + + @Test + public void + getScanner_CrudExceptionThrownByTransactionGetScanner_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + when(transaction.getScanner(scan)).thenThrow(CrudException.class); + + // Act Assert + assertThatThrownBy(() -> spied.getScanner(scan)).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerOne_CrudExceptionThrownByScannerOne_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + when(scanner.one()).thenThrow(CrudException.class); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::one).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerAll_CrudExceptionThrownByScannerAll_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + when(scanner.all()).thenThrow(CrudException.class); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::all).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_CrudExceptionThrownByScannerClose_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + doThrow(CrudException.class).when(scanner).close(); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_CommitConflictExceptionThrownByTransactionCommit_ShouldRollbackTransactionAndThrowCrudConflictException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(CommitConflictException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudConflictException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + + @Test + public void + getScannerAndScannerClose_UnknownTransactionStatusExceptionByTransactionCommit_ShouldThrowUnknownTransactionStatusException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(UnknownTransactionStatusException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(UnknownTransactionStatusException.class); + + verify(spied).begin(); + verify(scanner).close(); + } + + @Test + public void + getScannerAndScannerClose_CommitExceptionThrownByTransactionCommit_ShouldRollbackTransactionAndThrowCrudException() + throws TransactionException { + // Arrange + DistributedTransaction transaction = mock(DistributedTransaction.class); + + JdbcTransactionManager spied = spy(manager); + doReturn(transaction).when(spied).begin(); + doThrow(CommitException.class).when(transaction).commit(); + + Scan scan = + Scan.newBuilder() + .namespace(NAMESPACE) + .table(TABLE) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + TransactionCrudOperable.Scanner scanner = mock(TransactionCrudOperable.Scanner.class); + when(transaction.getScanner(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = spied.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + + verify(spied).begin(); + verify(scanner).close(); + verify(transaction).rollback(); + } + @Test public void whenPutOperationsExecutedAndJdbcServiceThrowsSQLException_shouldThrowCrudException() throws Exception { diff --git a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java index e09b04f8e4..2423457186 100644 --- a/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java +++ b/core/src/test/java/com/scalar/db/transaction/jdbc/JdbcTransactionTest.java @@ -1,8 +1,10 @@ package com.scalar.db.transaction.jdbc; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -12,6 +14,10 @@ import com.scalar.db.api.Insert; import com.scalar.db.api.MutationCondition; import com.scalar.db.api.Put; +import com.scalar.db.api.Result; +import com.scalar.db.api.Scan; +import com.scalar.db.api.Scanner; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; import com.scalar.db.exception.storage.ExecutionException; @@ -21,8 +27,12 @@ import com.scalar.db.io.Key; import com.scalar.db.storage.jdbc.JdbcService; import com.scalar.db.storage.jdbc.RdbEngineStrategy; +import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -70,6 +80,223 @@ public void setUp() throws Exception { transaction = new JdbcTransaction(ANY_TX_ID, jdbcService, connection, rdbEngineStrategy); } + @Test + public void getScannerAndScannerOne_ShouldReturnScannerAndShouldReturnProperResult() + throws SQLException, ExecutionException, CrudException, IOException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + Scanner scanner = mock(Scanner.class); + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + when(jdbcService.getScanner(scan, connection, false)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actual = transaction.getScanner(scan); + assertThat(actual.one()).hasValue(result1); + assertThat(actual.one()).hasValue(result2); + assertThat(actual.one()).hasValue(result3); + assertThat(actual.one()).isEmpty(); + actual.close(); + + verify(jdbcService).getScanner(scan, connection, false); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerAll_ShouldReturnScannerAndShouldReturnProperResults() + throws SQLException, ExecutionException, CrudException, IOException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + Scanner scanner = mock(Scanner.class); + when(scanner.all()).thenReturn(Arrays.asList(result1, result2, result3)); + + when(jdbcService.getScanner(scan, connection, false)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actual = transaction.getScanner(scan); + assertThat(actual.all()).containsExactly(result1, result2, result3); + actual.close(); + + verify(jdbcService).getScanner(scan, connection, false); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerIterator_ShouldReturnScannerAndShouldReturnProperResults() + throws SQLException, ExecutionException, CrudException, IOException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + Scanner scanner = mock(Scanner.class); + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + when(jdbcService.getScanner(scan, connection, false)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actual = transaction.getScanner(scan); + + Iterator iterator = actual.iterator(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result1); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result2); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result3); + assertThat(iterator.hasNext()).isFalse(); + actual.close(); + + verify(jdbcService).getScanner(scan, connection, false); + verify(scanner).close(); + } + + @Test + public void getScanner_WhenSQLExceptionThrownByJdbcService_ShouldThrowCrudException() + throws SQLException, ExecutionException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + when(jdbcService.getScanner(scan, connection, false)).thenThrow(SQLException.class); + + // Act Assert + assertThatThrownBy(() -> transaction.getScanner(scan)).isInstanceOf(CrudException.class); + } + + @Test + public void getScanner_WhenExecutionExceptionThrownByJdbcService_ShouldThrowCrudException() + throws SQLException, ExecutionException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + ExecutionException executionException = mock(ExecutionException.class); + when(executionException.getMessage()).thenReturn("error"); + when(jdbcService.getScanner(scan, connection, false)).thenThrow(executionException); + + // Act Assert + assertThatThrownBy(() -> transaction.getScanner(scan)).isInstanceOf(CrudException.class); + } + + @Test + public void + getScannerAndScannerOne_WhenExecutionExceptionThrownByScannerOne_ShouldThrowCrudException() + throws SQLException, ExecutionException, CrudException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + Scanner scanner = mock(Scanner.class); + + ExecutionException executionException = mock(ExecutionException.class); + when(executionException.getMessage()).thenReturn("error"); + when(scanner.one()).thenThrow(executionException); + + when(jdbcService.getScanner(scan, connection, false)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actual = transaction.getScanner(scan); + assertThatThrownBy(actual::one).isInstanceOf(CrudException.class); + } + + @Test + public void + getScannerAndScannerAll_WhenExecutionExceptionThrownByScannerAll_ShouldThrowCrudException() + throws SQLException, ExecutionException, CrudException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + Scanner scanner = mock(Scanner.class); + + ExecutionException executionException = mock(ExecutionException.class); + when(executionException.getMessage()).thenReturn("error"); + when(scanner.all()).thenThrow(executionException); + + when(jdbcService.getScanner(scan, connection, false)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actual = transaction.getScanner(scan); + assertThatThrownBy(actual::all).isInstanceOf(CrudException.class); + } + + @Test + public void + getScannerAndScannerClose_WhenIOExceptionThrownByScannerClose_ShouldThrowCrudException() + throws SQLException, ExecutionException, CrudException, IOException { + // Arrange + Scan scan = + Scan.newBuilder() + .namespace(ANY_NAMESPACE) + .table(ANY_TABLE_NAME) + .partitionKey(Key.ofText("p1", "val")) + .build(); + + Scanner scanner = mock(Scanner.class); + + IOException ioException = mock(IOException.class); + when(ioException.getMessage()).thenReturn("error"); + doThrow(ioException).when(scanner).close(); + + when(jdbcService.getScanner(scan, connection, false)).thenReturn(scanner); + + // Act Assert + TransactionCrudOperable.Scanner actual = transaction.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + } + @Test public void put_putDoesNotSucceed_shouldThrowUnsatisfiedConditionException() throws SQLException, ExecutionException { diff --git a/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java b/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java index c96d609485..42ef3d1c96 100644 --- a/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionManagerTest.java @@ -18,6 +18,7 @@ import com.scalar.db.api.Result; import com.scalar.db.api.Scan; import com.scalar.db.api.Scanner; +import com.scalar.db.api.TransactionManagerCrudOperable; import com.scalar.db.api.Update; import com.scalar.db.api.Upsert; import com.scalar.db.config.DatabaseConfig; @@ -28,7 +29,9 @@ import com.scalar.db.exception.transaction.TransactionException; import com.scalar.db.exception.transaction.UnsatisfiedConditionException; import com.scalar.db.io.Key; +import java.io.IOException; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; @@ -151,6 +154,178 @@ public void scan_ExecutionExceptionThrownByStorage_ShouldThrowCrudException() .hasCause(exception); } + @Test + public void getScannerAndScannerOne_ShouldReturnScannerAndShouldReturnProperResult() + throws ExecutionException, TransactionException, IOException { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + Scanner scanner = mock(Scanner.class); + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + when(storage.scan(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan); + assertThat(actual.one()).hasValue(result1); + assertThat(actual.one()).hasValue(result2); + assertThat(actual.one()).hasValue(result3); + assertThat(actual.one()).isEmpty(); + actual.close(); + + verify(storage).scan(scan); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerAll_ShouldReturnScannerAndShouldReturnProperResults() + throws ExecutionException, TransactionException, IOException { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + Scanner scanner = mock(Scanner.class); + when(scanner.all()).thenReturn(Arrays.asList(result1, result2, result3)); + + when(storage.scan(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan); + assertThat(actual.all()).containsExactly(result1, result2, result3); + actual.close(); + + verify(storage).scan(scan); + verify(scanner).close(); + } + + @Test + public void getScannerAndScannerIterator_ShouldReturnScannerAndShouldReturnProperResults() + throws ExecutionException, TransactionException, IOException { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + + Result result1 = mock(Result.class); + Result result2 = mock(Result.class); + Result result3 = mock(Result.class); + + Scanner scanner = mock(Scanner.class); + when(scanner.one()) + .thenReturn(Optional.of(result1)) + .thenReturn(Optional.of(result2)) + .thenReturn(Optional.of(result3)) + .thenReturn(Optional.empty()); + + when(storage.scan(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan); + + Iterator iterator = actual.iterator(); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result1); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result2); + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.next()).isEqualTo(result3); + assertThat(iterator.hasNext()).isFalse(); + actual.close(); + + verify(storage).scan(scan); + verify(scanner).close(); + } + + @Test + public void getScanner_WhenExecutionExceptionThrownByJdbcService_ShouldThrowCrudException() + throws ExecutionException { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + + ExecutionException executionException = mock(ExecutionException.class); + when(executionException.getMessage()).thenReturn("error"); + when(storage.scan(scan)).thenThrow(executionException); + + // Act Assert + assertThatThrownBy(() -> transactionManager.getScanner(scan)).isInstanceOf(CrudException.class); + } + + @Test + public void + getScannerAndScannerOne_WhenExecutionExceptionThrownByScannerOne_ShouldThrowCrudException() + throws ExecutionException, CrudException { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + + Scanner scanner = mock(Scanner.class); + + ExecutionException executionException = mock(ExecutionException.class); + when(executionException.getMessage()).thenReturn("error"); + when(scanner.one()).thenThrow(executionException); + + when(storage.scan(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan); + assertThatThrownBy(actual::one).isInstanceOf(CrudException.class); + } + + @Test + public void + getScannerAndScannerAll_WhenExecutionExceptionThrownByScannerAll_ShouldThrowCrudException() + throws ExecutionException, CrudException { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + + Scanner scanner = mock(Scanner.class); + + ExecutionException executionException = mock(ExecutionException.class); + when(executionException.getMessage()).thenReturn("error"); + when(scanner.all()).thenThrow(executionException); + + when(storage.scan(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan); + assertThatThrownBy(actual::all).isInstanceOf(CrudException.class); + } + + @Test + public void + getScannerAndScannerClose_WhenIOExceptionThrownByScannerClose_ShouldThrowCrudException() + throws ExecutionException, CrudException, IOException { + // Arrange + Scan scan = + Scan.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofInt("id", 0)).build(); + + Scanner scanner = mock(Scanner.class); + + IOException ioException = mock(IOException.class); + when(ioException.getMessage()).thenReturn("error"); + doThrow(ioException).when(scanner).close(); + + when(storage.scan(scan)).thenReturn(scanner); + + // Act Assert + TransactionManagerCrudOperable.Scanner actual = transactionManager.getScanner(scan); + assertThatThrownBy(actual::close).isInstanceOf(CrudException.class); + } + @Test public void put_ShouldCallStorageProperly() throws ExecutionException, TransactionException { // Arrange diff --git a/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java index 7779090ece..2577ca9163 100644 --- a/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/DistributedTransactionIntegrationTestBase.java @@ -57,6 +57,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -273,15 +275,17 @@ public void get_GetWithUnmatchedConjunctionsGivenForCommittedRecord_ShouldReturn assertThat(result.isPresent()).isFalse(); } - @Test - public void scan_ScanGivenForCommittedRecord_ShouldReturnRecords() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForCommittedRecord_ShouldReturnRecords(ScanType scanType) + throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); Scan scan = prepareScan(1, 0, 2); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -291,9 +295,10 @@ public void scan_ScanGivenForCommittedRecord_ShouldReturnRecords() throws Transa assertResult(1, 2, results.get(2)); } - @Test - public void scan_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); @@ -303,7 +308,7 @@ public void scan_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords .build(); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -320,9 +325,10 @@ public void scan_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords assertThat(results.get(1).getInt(SOME_COLUMN)).isEqualTo(2); } - @Test - public void scan_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); @@ -333,7 +339,7 @@ public void scan_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords( .withProjection(BALANCE); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -354,16 +360,17 @@ public void scan_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords( assertThat(results.get(2).getInt(ACCOUNT_TYPE)).isEqualTo(2); } - @Test - public void scan_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); Scan scan = prepareScan(1, 0, 2).withOrdering(Ordering.desc(ACCOUNT_TYPE)); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -384,16 +391,17 @@ public void scan_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords() assertThat(results.get(2).getInt(SOME_COLUMN)).isEqualTo(0); } - @Test - public void scan_ScanWithLimitGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithLimitGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); Scan scan = prepareScan(1, 0, 2).withLimit(2); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -424,15 +432,17 @@ public void get_GetGivenForNonExisting_ShouldReturnEmpty() throws TransactionExc assertThat(result.isPresent()).isFalse(); } - @Test - public void scan_ScanGivenForNonExisting_ShouldReturnEmpty() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForNonExisting_ShouldReturnEmpty(ScanType scanType) + throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); Scan scan = prepareScan(0, 4, 6); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -484,8 +494,10 @@ public void get_GetGivenForIndexColumn_ShouldReturnRecords() throws TransactionE assertThat(result2).isEqualTo(result1); } - @Test - public void scan_ScanGivenForIndexColumn_ShouldReturnRecords() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForIndexColumn_ShouldReturnRecords(ScanType scanType) + throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); @@ -517,8 +529,8 @@ public void scan_ScanGivenForIndexColumn_ShouldReturnRecords() throws Transactio .build()); // Act - List results1 = transaction.scan(scanBuiltByConstructor); - List results2 = transaction.scan(scanBuiltByBuilder); + List results1 = scanOrGetScanner(transaction, scanBuiltByConstructor, scanType); + List results2 = scanOrGetScanner(transaction, scanBuiltByBuilder, scanType); transaction.commit(); // Assert @@ -526,9 +538,10 @@ public void scan_ScanGivenForIndexColumn_ShouldReturnRecords() throws Transactio TestUtils.assertResultsContainsExactlyInAnyOrder(results2, expectedResults); } - @Test - public void scan_ScanGivenForIndexColumnWithConjunctions_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForIndexColumnWithConjunctions_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); @@ -541,7 +554,7 @@ public void scan_ScanGivenForIndexColumnWithConjunctions_ShouldReturnRecords() .build(); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -554,8 +567,9 @@ public void scan_ScanGivenForIndexColumnWithConjunctions_ShouldReturnRecords() assertThat(results.get(0).getInt(SOME_COLUMN)).isEqualTo(6); } - @Test - public void scan_ScanAllGivenForCommittedRecord_ShouldReturnRecords() + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenForCommittedRecord_ShouldReturnRecords(ScanType scanType) throws TransactionException { // Arrange populateRecords(); @@ -563,7 +577,7 @@ public void scan_ScanAllGivenForCommittedRecord_ShouldReturnRecords() ScanAll scanAll = prepareScanAll(); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.commit(); // Assert @@ -583,9 +597,10 @@ public void scan_ScanAllGivenForCommittedRecord_ShouldReturnRecords() TestUtils.assertResultsContainsExactlyInAnyOrder(results, expectedResults); } - @Test - public void scan_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords( + ScanType scanType) throws TransactionException { // Arrange insert(prepareInsert(1, 1), prepareInsert(1, 2), prepareInsert(2, 1), prepareInsert(3, 0)); @@ -593,7 +608,7 @@ public void scan_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords() ScanAll scanAll = prepareScanAll().withLimit(2); // Act - List results = scanAllTransaction.scan(scanAll); + List results = scanOrGetScanner(scanAllTransaction, scanAll, scanType); scanAllTransaction.commit(); // Assert @@ -623,16 +638,17 @@ public void scan_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords() assertThat(results).hasSize(2); } - @Test - public void scan_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues( + ScanType scanType) throws TransactionException { // Arrange populateRecords(); DistributedTransaction transaction = manager.start(); ScanAll scanAll = prepareScanAll().withProjection(ACCOUNT_TYPE).withProjection(BALANCE); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.commit(); // Assert @@ -651,14 +667,16 @@ public void scan_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues() TestUtils.assertResultsContainsExactlyInAnyOrder(results, expectedResults); } - @Test - public void scanAll_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenForNonExisting_ShouldReturnEmpty(ScanType scanType) + throws TransactionException { // Arrange DistributedTransaction transaction = manager.start(); ScanAll scanAll = prepareScanAll(); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.commit(); // Assert @@ -1048,17 +1066,18 @@ public void rollback_forOngoingTransaction_ShouldRollbackCorrectly() throws Tran assertThat(result.get().isNull(SOME_COLUMN)).isTrue(); } - @Test + @ParameterizedTest + @EnumSource(ScanType.class) public void - scan_ScanWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns() - throws TransactionException { + scanOrGetScanner_ScanWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns( + ScanType scanType) throws TransactionException { // Arrange populateSingleRecord(); DistributedTransaction transaction = manager.begin(); Scan scan = prepareScan(0, 0, 0).withProjections(Arrays.asList(BALANCE, SOME_COLUMN)); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.commit(); // Assert @@ -1070,17 +1089,18 @@ public void rollback_forOngoingTransaction_ShouldRollbackCorrectly() throws Tran }); } - @Test + @ParameterizedTest + @EnumSource(ScanType.class) public void - scan_ScanAllWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns() - throws TransactionException { + scanOrGetScanner_ScanAllWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns( + ScanType scanType) throws TransactionException { // Arrange populateSingleRecord(); DistributedTransaction transaction = manager.begin(); ScanAll scanAll = prepareScanAll().withProjections(Arrays.asList(BALANCE, SOME_COLUMN)); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.commit(); // Assert @@ -1093,6 +1113,30 @@ public void rollback_forOngoingTransaction_ShouldRollbackCorrectly() throws Tran results, Collections.singletonList(expectedResult)); } + @Test + public void getScanner_ScanGivenForCommittedRecord_ShouldReturnRecords() + throws TransactionException { + // Arrange + populateRecords(); + DistributedTransaction transaction = manager.start(); + Scan scan = prepareScan(0, 0, 2); + + // Act Assert + TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); + + Optional result1 = scanner.one(); + assertThat(result1).isPresent(); + assertResult(0, 0, result1.get()); + + Optional result2 = scanner.one(); + assertThat(result2).isPresent(); + assertResult(0, 1, result2.get()); + + scanner.close(); + + transaction.commit(); + } + @Test public void resume_WithBeginningTransaction_ShouldReturnBegunTransaction() throws TransactionException { @@ -1189,6 +1233,29 @@ public void scan_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionEx } } + @Test + public void getScanner_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { + Properties properties = getProperties(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace); + try (DistributedTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTransactionManager()) { + // Arrange + populateRecords(); + Scan scan = Scan.newBuilder().table(TABLE).all().build(); + + // Act Assert + Assertions.assertThatCode( + () -> { + DistributedTransaction tx = managerWithDefaultNamespace.start(); + TransactionCrudOperable.Scanner scanner = tx.getScanner(scan); + scanner.all(); + scanner.close(); + tx.commit(); + }) + .doesNotThrowAnyException(); + } + } + @Test public void put_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties(getTestName()); @@ -2006,6 +2073,42 @@ public void manager_scan_ScanGivenForCommittedRecord_ShouldReturnRecords() assertThat(results.get(2).getInt(SOME_COLUMN)).isEqualTo(2); } + @Test + public void manager_getScanner_ScanGivenForCommittedRecord_ShouldReturnRecords() + throws TransactionException { + // Arrange + populateRecords(); + Scan scan = prepareScan(1, 0, 2); + + // 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); + assertThat(result1.get().getInt(SOME_COLUMN)).isEqualTo(0); + + 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); + assertThat(result2.get().getInt(SOME_COLUMN)).isEqualTo(1); + + 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(result3.get().getInt(SOME_COLUMN)).isEqualTo(2); + + assertThat(scanner.one()).isNotPresent(); + + scanner.close(); + } + @Test public void manager_put_PutGivenForNonExisting_ShouldCreateRecord() throws TransactionException { // Arrange @@ -2250,6 +2353,30 @@ public void manager_scan_DefaultNamespaceGiven_ShouldWorkProperly() throws Trans } } + @Test + public void manager_getScanner_DefaultNamespaceGiven_ShouldWorkProperly() + throws TransactionException { + // Arrange + Properties properties = getProperties(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace); + try (DistributedTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTransactionManager()) { + // Arrange + populateRecords(); + Scan scan = Scan.newBuilder().table(TABLE).all().build(); + + // Act Assert + Assertions.assertThatCode( + () -> { + TransactionManagerCrudOperable.Scanner scanner = + managerWithDefaultNamespace.getScanner(scan); + scanner.all(); + scanner.close(); + }) + .doesNotThrowAnyException(); + } + } + @Test public void manager_put_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties(getTestName()); @@ -2670,4 +2797,35 @@ protected List> prepareNonKeyColumns(int accountId, int accountType) { } return columns.build(); } + + protected List scanOrGetScanner( + DistributedTransaction transaction, Scan scan, ScanType scanType) throws CrudException { + if (scanType == ScanType.SCAN) { + return transaction.scan(scan); + } + + try (TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan)) { + switch (scanType) { + case SCANNER_ONE: + List results = new ArrayList<>(); + while (true) { + Optional result = scanner.one(); + if (!result.isPresent()) { + return results; + } + results.add(result.get()); + } + case SCANNER_ALL: + return scanner.all(); + default: + throw new AssertionError(); + } + } + } + + public enum ScanType { + SCAN, + SCANNER_ONE, + SCANNER_ALL + } } diff --git a/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java index 6587cd79a1..c81bfe92d2 100644 --- a/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/api/TwoPhaseCommitTransactionIntegrationTestBase.java @@ -57,6 +57,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -312,15 +314,17 @@ public void get_GetWithUnmatchedConjunctionsGivenForCommittedRecord_ShouldReturn assertThat(result.isPresent()).isFalse(); } - @Test - public void scan_ScanGivenForCommittedRecord_ShouldReturnRecords() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForCommittedRecord_ShouldReturnRecords(ScanType scanType) + throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.start(); Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -332,9 +336,10 @@ public void scan_ScanGivenForCommittedRecord_ShouldReturnRecords() throws Transa assertResult(1, 2, results.get(2)); } - @Test - public void scan_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.start(); @@ -344,7 +349,7 @@ public void scan_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords .build(); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -363,9 +368,10 @@ public void scan_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords assertThat(results.get(1).getInt(SOME_COLUMN)).isEqualTo(2); } - @Test - public void scan_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.start(); @@ -376,7 +382,7 @@ public void scan_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords( .withProjection(BALANCE); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -399,16 +405,17 @@ public void scan_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords( assertThat(results.get(2).contains(SOME_COLUMN)).isFalse(); } - @Test - public void scan_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.start(); Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1).withOrdering(Ordering.desc(ACCOUNT_TYPE)); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -431,16 +438,17 @@ public void scan_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords() assertThat(results.get(2).getInt(SOME_COLUMN)).isEqualTo(0); } - @Test - public void scan_ScanWithLimitGivenForCommittedRecord_ShouldReturnRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithLimitGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.start(); Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1).withLimit(2); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -475,15 +483,17 @@ public void get_GetGivenForNonExisting_ShouldReturnEmpty() throws TransactionExc assertThat(result.isPresent()).isFalse(); } - @Test - public void scan_ScanGivenForNonExisting_ShouldReturnEmpty() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForNonExisting_ShouldReturnEmpty(ScanType scanType) + throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.start(); Scan scan = prepareScan(0, 4, 4, namespace1, TABLE_1); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -541,8 +551,10 @@ public void get_GetGivenForIndexColumn_ShouldReturnRecords() throws TransactionE assertThat(result2).isEqualTo(result1); } - @Test - public void scan_ScanGivenForIndexColumn_ShouldReturnRecords() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForIndexColumn_ShouldReturnRecords(ScanType scanType) + throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.start(); @@ -574,8 +586,8 @@ public void scan_ScanGivenForIndexColumn_ShouldReturnRecords() throws Transactio .build()); // Act - List results1 = transaction.scan(scanBuiltByConstructor); - List results2 = transaction.scan(scanBuiltByBuilder); + List results1 = scanOrGetScanner(transaction, scanBuiltByConstructor, scanType); + List results2 = scanOrGetScanner(transaction, scanBuiltByBuilder, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -1118,8 +1130,9 @@ public void abort_forOngoingTransaction_ShouldAbortCorrectly() throws Transactio assertThat(state).isEqualTo(TransactionState.ABORTED); } - @Test - public void scan_ScanAllGivenForCommittedRecord_ShouldReturnRecords() + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenForCommittedRecord_ShouldReturnRecords(ScanType scanType) throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); @@ -1127,7 +1140,7 @@ public void scan_ScanAllGivenForCommittedRecord_ShouldReturnRecords() ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -1149,9 +1162,10 @@ public void scan_ScanAllGivenForCommittedRecord_ShouldReturnRecords() TestUtils.assertResultsContainsExactlyInAnyOrder(results, expectedResults); } - @Test - public void scan_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords( + ScanType scanType) throws TransactionException { // Arrange TwoPhaseCommitTransaction putTransaction = manager1.begin(); insert( @@ -1167,7 +1181,7 @@ public void scan_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords() ScanAll scanAll = prepareScanAll(namespace1, TABLE_1).withLimit(2); // Act - List results = scanAllTransaction.scan(scanAll); + List results = scanOrGetScanner(scanAllTransaction, scanAll, scanType); scanAllTransaction.prepare(); scanAllTransaction.validate(); scanAllTransaction.commit(); @@ -1199,9 +1213,10 @@ public void scan_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords() assertThat(results).hasSize(2); } - @Test - public void scan_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues() - throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues( + ScanType scanType) throws TransactionException { // Arrange populateRecords(manager1, namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.begin(); @@ -1209,7 +1224,7 @@ public void scan_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues() prepareScanAll(namespace1, TABLE_1).withProjection(ACCOUNT_TYPE).withProjection(BALANCE); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -1235,14 +1250,16 @@ public void scan_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues() }); } - @Test - public void scan_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws TransactionException { + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenForNonExisting_ShouldReturnEmpty(ScanType scanType) + throws TransactionException { // Arrange TwoPhaseCommitTransaction transaction = manager1.begin(); ScanAll scanAll = prepareScanAll(namespace1, TABLE_1); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -1274,10 +1291,11 @@ public void scan_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws Transacti assertThat(result.get().isNull(SOME_COLUMN)).isTrue(); } - @Test + @ParameterizedTest + @EnumSource(ScanType.class) public void - scan_ScanWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns() - throws TransactionException { + scanOrGetScanner_ScanWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns( + ScanType scanType) throws TransactionException { // Arrange TwoPhaseCommitTransaction transaction = manager1.begin(); populateSingleRecord(namespace1, TABLE_1); @@ -1286,7 +1304,7 @@ public void scan_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws Transacti .withProjections(Arrays.asList(BALANCE, SOME_COLUMN)); // Act - List results = transaction.scan(scan); + List results = scanOrGetScanner(transaction, scan, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -1300,10 +1318,11 @@ public void scan_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws Transacti }); } - @Test + @ParameterizedTest + @EnumSource(ScanType.class) public void - scan_ScanAllWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns() - throws TransactionException { + scanOrGetScanner_ScanAllWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns( + ScanType scanType) throws TransactionException { // Arrange populateSingleRecord(namespace1, TABLE_1); TwoPhaseCommitTransaction transaction = manager1.begin(); @@ -1311,7 +1330,7 @@ public void scan_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws Transacti prepareScanAll(namespace1, TABLE_1).withProjections(Arrays.asList(BALANCE, SOME_COLUMN)); // Act - List results = transaction.scan(scanAll); + List results = scanOrGetScanner(transaction, scanAll, scanType); transaction.prepare(); transaction.validate(); transaction.commit(); @@ -1326,6 +1345,32 @@ public void scan_ScanAllGivenForNonExisting_ShouldReturnEmpty() throws Transacti results, Collections.singletonList(expectedResult)); } + @Test + public void getScanner_ScanGivenForCommittedRecord_ShouldReturnRecords() + throws TransactionException { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + TwoPhaseCommitTransaction transaction = manager1.start(); + Scan scan = prepareScan(0, 0, 2, namespace1, TABLE_1); + + // Act Assert + TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan); + + Optional result1 = scanner.one(); + assertThat(result1).isPresent(); + assertResult(0, 0, result1.get()); + + Optional result2 = scanner.one(); + assertThat(result2).isPresent(); + assertResult(0, 1, result2.get()); + + scanner.close(); + + transaction.prepare(); + transaction.validate(); + transaction.commit(); + } + @Test public void resume_WithBeginningTransaction_ShouldReturnBegunTransaction() throws TransactionException { @@ -1381,8 +1426,8 @@ public void resume_WithBeginningAndCommittingTransaction_ShouldThrowTransactionN public void get_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Get get = @@ -1395,8 +1440,10 @@ public void get_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionExc // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.get(get); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -1407,8 +1454,8 @@ public void get_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionExc public void scan_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Scan scan = Scan.newBuilder().table(TABLE_1).all().build(); @@ -1416,8 +1463,35 @@ public void scan_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionEx // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.scan(scan); + tx.prepare(); + tx.validate(); + tx.commit(); + }) + .doesNotThrowAnyException(); + } + } + + @Test + public void getScanner_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { + Properties properties = getProperties1(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + Scan scan = Scan.newBuilder().table(TABLE_1).all().build(); + + // Act Assert + Assertions.assertThatCode( + () -> { + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); + TransactionCrudOperable.Scanner scanner = tx.getScanner(scan); + scanner.all(); + scanner.close(); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -1428,8 +1502,8 @@ public void scan_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionEx public void put_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Put put = @@ -1444,8 +1518,10 @@ public void put_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionExc // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.put(put); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -1456,8 +1532,8 @@ public void put_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionExc public void insert_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Insert insert = @@ -1471,8 +1547,10 @@ public void insert_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.insert(insert); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -1483,8 +1561,8 @@ public void insert_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction public void upsert_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Upsert upsert = @@ -1498,8 +1576,10 @@ public void upsert_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.upsert(upsert); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -1510,8 +1590,8 @@ public void upsert_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction public void update_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Update update = @@ -1525,8 +1605,10 @@ public void update_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.update(update); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -1537,8 +1619,8 @@ public void update_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction public void delete_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Delete delete = @@ -1551,8 +1633,10 @@ public void delete_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.delete(delete); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -1563,8 +1647,8 @@ public void delete_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction public void mutate_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Mutation putAsMutation1 = @@ -1585,8 +1669,10 @@ public void mutate_DefaultNamespaceGiven_ShouldWorkProperly() throws Transaction // Act Assert Assertions.assertThatCode( () -> { - DistributedTransaction tx = managerWithDefaultNamespace.start(); + TwoPhaseCommitTransaction tx = managerWithDefaultNamespace.start(); tx.mutate(ImmutableList.of(putAsMutation1, deleteAsMutation2)); + tx.prepare(); + tx.validate(); tx.commit(); }) .doesNotThrowAnyException(); @@ -2279,6 +2365,42 @@ public void manager_scan_ScanGivenForCommittedRecord_ShouldReturnRecords() assertThat(results.get(2).getInt(SOME_COLUMN)).isEqualTo(2); } + @Test + public void manager_getScanner_ScanGivenForCommittedRecord_ShouldReturnRecords() + throws TransactionException { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + Scan scan = prepareScan(1, 0, 2, namespace1, TABLE_1); + + // Act Assert + TransactionManagerCrudOperable.Scanner scanner = manager1.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); + assertThat(result1.get().getInt(SOME_COLUMN)).isEqualTo(0); + + 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); + assertThat(result2.get().getInt(SOME_COLUMN)).isEqualTo(1); + + 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(result3.get().getInt(SOME_COLUMN)).isEqualTo(2); + + assertThat(scanner.one()).isNotPresent(); + + scanner.close(); + } + @Test public void manager_put_PutGivenForNonExisting_ShouldCreateRecord() throws TransactionException { // Arrange @@ -2490,8 +2612,8 @@ public void manager_delete_DeleteGivenForExisting_ShouldDeleteRecord() public void manager_get_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Get get = @@ -2511,8 +2633,8 @@ public void manager_get_DefaultNamespaceGiven_ShouldWorkProperly() throws Transa public void manager_scan_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Scan scan = Scan.newBuilder().table(TABLE_1).all().build(); @@ -2523,12 +2645,35 @@ public void manager_scan_DefaultNamespaceGiven_ShouldWorkProperly() throws Trans } } + @Test + public void manager_getScanner_DefaultNamespaceGiven_ShouldWorkProperly() + throws TransactionException { + Properties properties = getProperties1(getTestName()); + properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { + // Arrange + populateRecords(manager1, namespace1, TABLE_1); + Scan scan = Scan.newBuilder().table(TABLE_1).all().build(); + + // Act Assert + Assertions.assertThatCode( + () -> { + TransactionManagerCrudOperable.Scanner scanner = + managerWithDefaultNamespace.getScanner(scan); + scanner.all(); + scanner.close(); + }) + .doesNotThrowAnyException(); + } + } + @Test public void manager_put_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Put put = @@ -2551,8 +2696,8 @@ public void manager_insert_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Insert insert = @@ -2574,8 +2719,8 @@ public void manager_upsert_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Upsert upsert = @@ -2597,8 +2742,8 @@ public void manager_update_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Update update = @@ -2620,8 +2765,8 @@ public void manager_delete_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Delete delete = @@ -2642,8 +2787,8 @@ public void manager_mutate_DefaultNamespaceGiven_ShouldWorkProperly() throws TransactionException { Properties properties = getProperties1(getTestName()); properties.put(DatabaseConfig.DEFAULT_NAMESPACE_NAME, namespace1); - try (DistributedTransactionManager managerWithDefaultNamespace = - TransactionFactory.create(properties).getTransactionManager()) { + try (TwoPhaseCommitTransactionManager managerWithDefaultNamespace = + TransactionFactory.create(properties).getTwoPhaseCommitTransactionManager()) { // Arrange populateRecords(manager1, namespace1, TABLE_1); Mutation putAsMutation1 = @@ -2972,4 +3117,35 @@ protected List> prepareNonKeyColumns(int accountId, int accountType) { } return columns.build(); } + + protected List scanOrGetScanner( + TwoPhaseCommitTransaction transaction, Scan scan, ScanType scanType) throws CrudException { + if (scanType == ScanType.SCAN) { + return transaction.scan(scan); + } + + try (TransactionCrudOperable.Scanner scanner = transaction.getScanner(scan)) { + switch (scanType) { + case SCANNER_ONE: + List results = new ArrayList<>(); + while (true) { + Optional result = scanner.one(); + if (!result.isPresent()) { + return results; + } + results.add(result.get()); + } + case SCANNER_ALL: + return scanner.all(); + default: + throw new AssertionError(); + } + } + } + + public enum ScanType { + SCAN, + SCANNER_ONE, + SCANNER_ALL + } } 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 428fce3e35..bbf50ff817 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 @@ -32,6 +32,7 @@ import com.scalar.db.api.ScanAll; import com.scalar.db.api.Selection; import com.scalar.db.api.TableMetadata; +import com.scalar.db.api.TransactionCrudOperable; import com.scalar.db.api.TransactionState; import com.scalar.db.api.Update; import com.scalar.db.config.DatabaseConfig; @@ -4364,6 +4365,220 @@ public void get_GetWithIndexGiven_WithSerializable_ShouldNotThrowAnyException() assertThatCode(transaction::commit).doesNotThrowAnyException(); } + @Test + public void getScanner_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(), + 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() diff --git a/core/src/integration-test/java/com/scalar/db/common/ConsensusCommitTestUtils.java b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTestUtils.java similarity index 98% rename from core/src/integration-test/java/com/scalar/db/common/ConsensusCommitTestUtils.java rename to integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTestUtils.java index 844b3276c0..f7ec3b18b8 100644 --- a/core/src/integration-test/java/com/scalar/db/common/ConsensusCommitTestUtils.java +++ b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTestUtils.java @@ -1,4 +1,4 @@ -package com.scalar.db.common; +package com.scalar.db.transaction.consensuscommit; import static com.scalar.db.transaction.consensuscommit.ConsensusCommitConfig.COORDINATOR_GROUP_COMMIT_DELAYED_SLOT_MOVE_TIMEOUT_MILLIS; import static com.scalar.db.transaction.consensuscommit.ConsensusCommitConfig.COORDINATOR_GROUP_COMMIT_ENABLED; diff --git a/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java index d9c27237c1..e9df3c8941 100644 --- a/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/transaction/singlecrudoperation/SingleCrudOperationTransactionIntegrationTestBase.java @@ -9,6 +9,8 @@ import java.util.Properties; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; public abstract class SingleCrudOperationTransactionIntegrationTestBase extends DistributedTransactionIntegrationTestBase { @@ -70,23 +72,30 @@ public void get_GetWithUnmatchedConjunctionsGivenForCommittedRecord_ShouldReturn @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanGivenForCommittedRecord_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForCommittedRecord_ShouldReturnRecords(ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithProjectionsGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithOrderingGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanWithLimitGivenForCommittedRecord_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithLimitGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @@ -95,8 +104,9 @@ public void get_GetGivenForNonExisting_ShouldReturnEmpty() {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanGivenForNonExisting_ShouldReturnEmpty() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForNonExisting_ShouldReturnEmpty(ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @@ -105,28 +115,41 @@ public void get_GetGivenForIndexColumn_ShouldReturnRecords() {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanGivenForIndexColumn_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForIndexColumn_ShouldReturnRecords(ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanAllGivenForCommittedRecord_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenWithLimit_ShouldReturnLimitedAmountOfRecords( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllWithProjectionsGiven_ShouldRetrieveSpecifiedValues( + ScanType scanType) {} + + @Disabled("Single CRUD operation transactions don't support beginning a transaction") + @Override + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanAllGivenForNonExisting_ShouldReturnEmpty(ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @Test - public void scanAll_ScanAllGivenForNonExisting_ShouldReturnEmpty() {} + public void getScanner_ScanGivenForCommittedRecord_ShouldReturnRecords() {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @@ -196,15 +219,19 @@ public void mutateAndCommit_ShouldMutateRecordsProperly() {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test + @ParameterizedTest + @EnumSource(ScanType.class) public void - scan_ScanWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns() {} + scanOrGetScanner_ScanWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test + @ParameterizedTest + @EnumSource(ScanType.class) public void - scan_ScanAllWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns() {} + scanOrGetScanner_ScanAllWithProjectionsGivenOnNonPrimaryKeyColumnsForCommittedRecord_ShouldReturnOnlyProjectedColumns( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support resuming a transaction") @Override @@ -238,6 +265,11 @@ public void get_DefaultNamespaceGiven_ShouldWorkProperly() {} @Test public void scan_DefaultNamespaceGiven_ShouldWorkProperly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") + @Override + @Test + public void getScanner_DefaultNamespaceGiven_ShouldWorkProperly() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @Test @@ -413,11 +445,15 @@ public void manager_mutate_DefaultNamespaceGiven_ShouldWorkProperly() {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords( + ScanType scanType) {} @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override - @Test - public void scan_ScanGivenForIndexColumnWithConjunctions_ShouldReturnRecords() {} + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForIndexColumnWithConjunctions_ShouldReturnRecords( + ScanType scanType) {} }