From 2b596dd28a1b370b4c4dececebb12b263a8bdf59 Mon Sep 17 00:00:00 2001 From: brfrn169 Date: Tue, 13 May 2025 15:01:55 +0900 Subject: [PATCH] Add beginReadOnly() method to transaction abstraction --- .../jdbc/JdbcTransactionIntegrationTest.java | 14 ++++ .../db/api/DistributedTransactionManager.java | 61 +++++++++++++++ ...nManagedDistributedTransactionManager.java | 2 + ...nagedTwoPhaseCommitTransactionManager.java | 2 + ...ecoratedDistributedTransactionManager.java | 20 +++++ .../ReadOnlyDistributedTransaction.java | 75 +++++++++++++++++++ ...eManagedDistributedTransactionManager.java | 2 + ...nagedTwoPhaseCommitTransactionManager.java | 2 + .../com/scalar/db/common/error/CoreError.java | 6 ++ .../scalar/db/service/TransactionService.java | 20 +++++ .../ConsensusCommitManager.java | 10 +++ .../jdbc/JdbcTransactionManager.java | 10 +++ ...SingleCrudOperationTransactionManager.java | 14 ++++ ...ributedTransactionIntegrationTestBase.java | 36 +++++++++ .../ConsensusCommitIntegrationTestBase.java | 15 ++++ ...erationTransactionIntegrationTestBase.java | 12 +++ 16 files changed, 301 insertions(+) create mode 100644 core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java 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 37bebaf726..fa67a2d870 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 @@ -7,6 +7,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 class JdbcTransactionIntegrationTest extends DistributedTransactionIntegrationTestBase { @@ -42,4 +44,16 @@ public void abort_forOngoingTransaction_ShouldAbortCorrectly() {} @Override @Test public void rollback_forOngoingTransaction_ShouldRollbackCorrectly() {} + + @Disabled("Implement later") + @Override + @Test + public void get_GetGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecord() {} + + @Disabled("Implement later") + @Override + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecords( + ScanType scanType) {} } diff --git a/core/src/main/java/com/scalar/db/api/DistributedTransactionManager.java b/core/src/main/java/com/scalar/db/api/DistributedTransactionManager.java index f9c84cfacf..c6262432fd 100644 --- a/core/src/main/java/com/scalar/db/api/DistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/api/DistributedTransactionManager.java @@ -81,6 +81,34 @@ public interface DistributedTransactionManager DistributedTransaction begin(String txId) throws TransactionNotFoundException, TransactionException; + /** + * Begins a new transaction in read-only mode. + * + * @return {@link DistributedTransaction} + * @throws TransactionNotFoundException if the transaction fails to begin due to transient faults. + * You can retry the transaction + * @throws TransactionException if the transaction fails to begin due to transient or nontransient + * faults. You can try retrying the transaction, but you may not be able to begin the + * transaction due to nontransient faults + */ + DistributedTransaction beginReadOnly() throws TransactionNotFoundException, TransactionException; + + /** + * Begins a new transaction with the specified transaction ID in read-only mode. It is users' + * responsibility to guarantee uniqueness of the ID, so it is not recommended to use this method + * unless you know exactly what you are doing. + * + * @param txId an user-provided unique transaction ID + * @return {@link DistributedTransaction} + * @throws TransactionNotFoundException if the transaction fails to begin due to transient faults. + * You can retry the transaction + * @throws TransactionException if the transaction fails to begin due to transient or nontransient + * faults. You can try retrying the transaction, but you may not be able to begin the + * transaction due to nontransient faults + */ + DistributedTransaction beginReadOnly(String txId) + throws TransactionNotFoundException, TransactionException; + /** * Starts a new transaction. This method is an alias of {@link #begin()}. * @@ -112,6 +140,39 @@ default DistributedTransaction start(String txId) return begin(txId); } + /** + * Starts a new transaction in read-only mode. This method is an alias of {@link + * #beginReadOnly()}. + * + * @return {@link DistributedTransaction} + * @throws TransactionNotFoundException if the transaction fails to start due to transient faults. + * You can retry the transaction + * @throws TransactionException if the transaction fails to start due to transient or nontransient + * faults. You can try retrying the transaction, but you may not be able to start the + * transaction due to nontransient faults + */ + default DistributedTransaction startReadOnly() + throws TransactionNotFoundException, TransactionException { + return beginReadOnly(); + } + + /** + * Starts a new transaction with the specified transaction ID in read-only mode. This method is an + * alias of {@link #beginReadOnly(String)}. + * + * @param txId an user-provided unique transaction ID + * @return {@link DistributedTransaction} + * @throws TransactionNotFoundException if the transaction fails to start due to transient faults. + * You can retry the transaction + * @throws TransactionException if the transaction fails to start due to transient or nontransient + * faults. You can try retrying the transaction, but you may not be able to start the + * transaction due to nontransient faults + */ + default DistributedTransaction startReadOnly(String txId) + throws TransactionNotFoundException, TransactionException { + return beginReadOnly(txId); + } + /** * Starts a new transaction with the specified {@link Isolation} level. * 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 ea592e5b41..5cce5603e4 100644 --- a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedDistributedTransactionManager.java @@ -26,9 +26,11 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import javax.annotation.concurrent.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@ThreadSafe public class ActiveTransactionManagedDistributedTransactionManager extends DecoratedDistributedTransactionManager { 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 b0543433d3..f1d52f897d 100644 --- a/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/ActiveTransactionManagedTwoPhaseCommitTransactionManager.java @@ -28,9 +28,11 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import javax.annotation.concurrent.ThreadSafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@ThreadSafe public class ActiveTransactionManagedTwoPhaseCommitTransactionManager extends DecoratedTwoPhaseCommitTransactionManager { 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 dac3cfa2c7..9ade75d900 100644 --- a/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/DecoratedDistributedTransactionManager.java @@ -76,6 +76,16 @@ public DistributedTransaction begin(String txId) throws TransactionException { return decorateTransactionOnBeginOrStart(transactionManager.begin(txId)); } + @Override + public DistributedTransaction beginReadOnly() throws TransactionException { + return decorateTransactionOnBeginOrStart(transactionManager.beginReadOnly()); + } + + @Override + public DistributedTransaction beginReadOnly(String txId) throws TransactionException { + return decorateTransactionOnBeginOrStart(transactionManager.beginReadOnly(txId)); + } + @Override public DistributedTransaction start() throws TransactionException { return decorateTransactionOnBeginOrStart(transactionManager.start()); @@ -86,6 +96,16 @@ public DistributedTransaction start(String txId) throws TransactionException { return decorateTransactionOnBeginOrStart(transactionManager.start(txId)); } + @Override + public DistributedTransaction startReadOnly(String txId) throws TransactionException { + return decorateTransactionOnBeginOrStart(transactionManager.startReadOnly(txId)); + } + + @Override + public DistributedTransaction startReadOnly() throws TransactionException { + return decorateTransactionOnBeginOrStart(transactionManager.startReadOnly()); + } + /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @Deprecated @Override diff --git a/core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java b/core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java new file mode 100644 index 0000000000..ffff08acaf --- /dev/null +++ b/core/src/main/java/com/scalar/db/common/ReadOnlyDistributedTransaction.java @@ -0,0 +1,75 @@ +package com.scalar.db.common; + +import com.scalar.db.api.Delete; +import com.scalar.db.api.DistributedTransaction; +import com.scalar.db.api.Insert; +import com.scalar.db.api.Mutation; +import com.scalar.db.api.Put; +import com.scalar.db.api.Update; +import com.scalar.db.api.Upsert; +import com.scalar.db.common.error.CoreError; +import com.scalar.db.exception.transaction.CrudException; +import java.util.List; +import javax.annotation.concurrent.NotThreadSafe; + +@NotThreadSafe +public class ReadOnlyDistributedTransaction extends DecoratedDistributedTransaction { + + public ReadOnlyDistributedTransaction(DistributedTransaction transaction) { + super(transaction); + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated + @Override + public void put(Put put) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated + @Override + public void put(List puts) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + + @Override + public void insert(Insert insert) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + + @Override + public void upsert(Upsert upsert) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + + @Override + public void update(Update update) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + + @Override + public void delete(Delete delete) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + + /** @deprecated As of release 3.13.0. Will be removed in release 5.0.0. */ + @Deprecated + @Override + public void delete(List deletes) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } + + @Override + public void mutate(List mutations) throws CrudException { + throw new IllegalStateException( + CoreError.MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION.buildMessage(getId())); + } +} 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 52866cb405..1dad240f24 100644 --- a/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/StateManagedDistributedTransactionManager.java @@ -20,7 +20,9 @@ import com.scalar.db.exception.transaction.UnknownTransactionStatusException; import java.util.List; import java.util.Optional; +import javax.annotation.concurrent.ThreadSafe; +@ThreadSafe public class StateManagedDistributedTransactionManager extends DecoratedDistributedTransactionManager { 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 1d79240d04..8e59e13a50 100644 --- a/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java +++ b/core/src/main/java/com/scalar/db/common/StateManagedTwoPhaseCommitTransactionManager.java @@ -22,7 +22,9 @@ import com.scalar.db.exception.transaction.ValidationException; import java.util.List; import java.util.Optional; +import javax.annotation.concurrent.ThreadSafe; +@ThreadSafe public class StateManagedTwoPhaseCommitTransactionManager extends DecoratedTwoPhaseCommitTransactionManager { 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 7c5a5fd1d4..9f4990f617 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 @@ -923,6 +923,12 @@ public enum CoreError implements ScalarDbError { "Some scanners were not closed. All scanners must be closed before preparing the transaction.", "", ""), + MUTATION_NOT_ALLOWED_IN_READ_ONLY_TRANSACTION( + Category.USER_ERROR, + "0207", + "Mutations are not allowed in read-only transactions. Transaction ID: %s", + "", + ""), // // Errors for the concurrency 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 8acc748eaa..492dc6e9b5 100644 --- a/core/src/main/java/com/scalar/db/service/TransactionService.java +++ b/core/src/main/java/com/scalar/db/service/TransactionService.java @@ -81,6 +81,16 @@ public DistributedTransaction begin(String txId) throws TransactionException { return manager.begin(txId); } + @Override + public DistributedTransaction beginReadOnly() throws TransactionException { + return manager.beginReadOnly(); + } + + @Override + public DistributedTransaction beginReadOnly(String txId) throws TransactionException { + return manager.beginReadOnly(txId); + } + @Override public DistributedTransaction start() throws TransactionException { return manager.start(); @@ -91,6 +101,16 @@ public DistributedTransaction start(String txId) throws TransactionException { return manager.start(txId); } + @Override + public DistributedTransaction startReadOnly() throws TransactionException { + return manager.startReadOnly(); + } + + @Override + public DistributedTransaction startReadOnly(String txId) throws TransactionException { + return manager.startReadOnly(txId); + } + /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @Deprecated @Override 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 9eff205532..6c553c53f7 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 @@ -145,6 +145,16 @@ public DistributedTransaction begin(String txId) { return begin(txId, config.getIsolation()); } + @Override + public DistributedTransaction beginReadOnly() { + throw new UnsupportedOperationException("implement later"); + } + + @Override + public DistributedTransaction beginReadOnly(String txId) { + throw new UnsupportedOperationException("implement later"); + } + /** @deprecated As of release 2.4.0. Will be removed in release 4.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 d65b512106..b38862f7ba 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 @@ -109,6 +109,16 @@ public DistributedTransaction begin(String txId) throws TransactionException { } } + @Override + public DistributedTransaction beginReadOnly() { + throw new UnsupportedOperationException("implement later"); + } + + @Override + public DistributedTransaction beginReadOnly(String txId) { + throw new UnsupportedOperationException("implement later"); + } + /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @SuppressWarnings("InlineMeSuggester") @Deprecated 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 cbc136d120..573aec61f7 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 @@ -76,6 +76,20 @@ public DistributedTransaction begin(String txId) throws TransactionException { .buildMessage()); } + @Override + public DistributedTransaction beginReadOnly() throws TransactionException { + throw new UnsupportedOperationException( + CoreError.SINGLE_CRUD_OPERATION_TRANSACTION_BEGINNING_TRANSACTION_NOT_ALLOWED + .buildMessage()); + } + + @Override + public DistributedTransaction beginReadOnly(String txId) throws TransactionException { + throw new UnsupportedOperationException( + CoreError.SINGLE_CRUD_OPERATION_TRANSACTION_BEGINNING_TRANSACTION_NOT_ALLOWED + .buildMessage()); + } + /** @deprecated As of release 2.4.0. Will be removed in release 4.0.0. */ @Deprecated @Override 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 2577ca9163..1ab6938241 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 @@ -197,6 +197,22 @@ public void get_GetGivenForCommittedRecord_ShouldReturnRecord() throws Transacti assertResult(2, 3, result); } + @Test + public void get_GetGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecord() + throws TransactionException { + // Arrange + populateRecords(); + DistributedTransaction transaction = manager.beginReadOnly(); + Get get = prepareGet(2, 3); + + // Act + Optional result = transaction.get(get); + transaction.commit(); + + // Assert + assertResult(2, 3, result); + } + @Test public void get_GetWithProjectionGivenForCommittedRecord_ShouldReturnRecord() throws TransactionException { @@ -295,6 +311,26 @@ public void scanOrGetScanner_ScanGivenForCommittedRecord_ShouldReturnRecords(Sca assertResult(1, 2, results.get(2)); } + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecords( + ScanType scanType) throws TransactionException { + // Arrange + populateRecords(); + DistributedTransaction transaction = manager.beginReadOnly(); + Scan scan = prepareScan(1, 0, 2); + + // Act + List results = scanOrGetScanner(transaction, scan, scanType); + transaction.commit(); + + // Assert + assertThat(results.size()).isEqualTo(3); + assertResult(1, 0, results.get(0)); + assertResult(1, 1, results.get(1)); + assertResult(1, 2, results.get(2)); + } + @ParameterizedTest @EnumSource(ScanType.class) public void scanOrGetScanner_ScanWithConjunctionsGivenForCommittedRecord_ShouldReturnRecords( diff --git a/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitIntegrationTestBase.java b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitIntegrationTestBase.java index bf5abaae05..e5216ce7e5 100644 --- a/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitIntegrationTestBase.java +++ b/integration-test/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitIntegrationTestBase.java @@ -15,7 +15,10 @@ import com.scalar.db.io.Key; import java.util.Optional; 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 ConsensusCommitIntegrationTestBase extends DistributedTransactionIntegrationTestBase { @@ -929,4 +932,16 @@ public void deleteAndDelete_forSameRecord_shouldWorkCorrectly() throws Transacti Optional optResult = get(prepareGet(0, 0)); assertThat(optResult).isNotPresent(); } + + @Disabled("Implement later") + @Override + @Test + public void get_GetGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecord() {} + + @Disabled("Implement later") + @Override + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecords( + ScanType scanType) {} } 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 e9df3c8941..5cc864af39 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 @@ -55,6 +55,11 @@ protected void populateRecords() throws TransactionException { @Test public void get_GetGivenForCommittedRecord_ShouldReturnRecord() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") + @Override + @Test + public void get_GetGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecord() {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @Test @@ -76,6 +81,13 @@ public void get_GetWithUnmatchedConjunctionsGivenForCommittedRecord_ShouldReturn @EnumSource(ScanType.class) public void scanOrGetScanner_ScanGivenForCommittedRecord_ShouldReturnRecords(ScanType scanType) {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") + @Override + @ParameterizedTest + @EnumSource(ScanType.class) + public void scanOrGetScanner_ScanGivenForCommittedRecord_InReadOnlyMode_ShouldReturnRecords( + ScanType scanType) {} + @Disabled("Single CRUD operation transactions don't support beginning a transaction") @Override @ParameterizedTest