diff --git a/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java b/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java index bcc16eb30c..a45c69e317 100644 --- a/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java +++ b/core/src/main/java/com/scalar/db/api/DistributedStorageAdmin.java @@ -47,6 +47,9 @@ public interface DistributedStorageAdmin extends Admin, AutoCloseable { /** * Returns the storage information. * + *
Note: This feature is primarily for internal use. Breaking changes can and will be
+ * introduced to it. Users should not depend on it.
+ *
* @param namespace the namespace to get the storage information for
* @return the storage information
* @throws ExecutionException if the operation fails
diff --git a/core/src/main/java/com/scalar/db/api/StorageInfo.java b/core/src/main/java/com/scalar/db/api/StorageInfo.java
index 507f974318..35a40dde2b 100644
--- a/core/src/main/java/com/scalar/db/api/StorageInfo.java
+++ b/core/src/main/java/com/scalar/db/api/StorageInfo.java
@@ -22,6 +22,13 @@ public interface StorageInfo {
*/
int getMaxAtomicMutationsCount();
+ /**
+ * Returns whether the storage guarantees consistent reads for virtual tables.
+ *
+ * @return true if the storage guarantees consistent reads for virtual tables, false otherwise
+ */
+ boolean isConsistentVirtualTableReadGuaranteed();
+
/**
* The mutation atomicity unit of the storage.
*
diff --git a/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java b/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java
index 2f3fc48a1e..e1f9bc2d7a 100644
--- a/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java
+++ b/core/src/main/java/com/scalar/db/common/StorageInfoImpl.java
@@ -11,14 +11,17 @@ public class StorageInfoImpl implements StorageInfo {
private final String storageName;
private final MutationAtomicityUnit mutationAtomicityUnit;
private final int maxAtomicMutationsCount;
+ private final boolean consistentVirtualTableReadGuaranteed;
public StorageInfoImpl(
String storageName,
MutationAtomicityUnit mutationAtomicityUnit,
- int maxAtomicMutationsCount) {
+ int maxAtomicMutationsCount,
+ boolean consistentVirtualTableReadGuaranteed) {
this.storageName = storageName;
this.mutationAtomicityUnit = mutationAtomicityUnit;
this.maxAtomicMutationsCount = maxAtomicMutationsCount;
+ this.consistentVirtualTableReadGuaranteed = consistentVirtualTableReadGuaranteed;
}
@Override
@@ -36,6 +39,11 @@ public int getMaxAtomicMutationsCount() {
return maxAtomicMutationsCount;
}
+ @Override
+ public boolean isConsistentVirtualTableReadGuaranteed() {
+ return consistentVirtualTableReadGuaranteed;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -47,12 +55,18 @@ public boolean equals(Object o) {
StorageInfoImpl that = (StorageInfoImpl) o;
return getMaxAtomicMutationsCount() == that.getMaxAtomicMutationsCount()
&& Objects.equals(getStorageName(), that.getStorageName())
- && getMutationAtomicityUnit() == that.getMutationAtomicityUnit();
+ && getMutationAtomicityUnit() == that.getMutationAtomicityUnit()
+ && isConsistentVirtualTableReadGuaranteed()
+ == that.isConsistentVirtualTableReadGuaranteed();
}
@Override
public int hashCode() {
- return Objects.hash(getStorageName(), getMutationAtomicityUnit(), getMaxAtomicMutationsCount());
+ return Objects.hash(
+ getStorageName(),
+ getMutationAtomicityUnit(),
+ getMaxAtomicMutationsCount(),
+ isConsistentVirtualTableReadGuaranteed());
}
@Override
@@ -61,6 +75,7 @@ public String toString() {
.add("storageName", storageName)
.add("mutationAtomicityUnit", mutationAtomicityUnit)
.add("maxAtomicMutationsCount", maxAtomicMutationsCount)
+ .add("consistentVirtualTableReadGuaranteed", consistentVirtualTableReadGuaranteed)
.toString();
}
}
diff --git a/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java b/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java
index 9a60eb15b9..b889c08a86 100644
--- a/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java
+++ b/core/src/main/java/com/scalar/db/storage/cassandra/CassandraAdmin.java
@@ -57,7 +57,8 @@ public class CassandraAdmin implements DistributedStorageAdmin {
"cassandra",
StorageInfo.MutationAtomicityUnit.PARTITION,
// No limit on the number of mutations
- Integer.MAX_VALUE);
+ Integer.MAX_VALUE,
+ false);
private final ClusterManager clusterManager;
private final String metadataKeyspace;
diff --git a/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java b/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java
index 3836b06443..30b14fce0d 100644
--- a/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java
+++ b/core/src/main/java/com/scalar/db/storage/cosmos/CosmosAdmin.java
@@ -74,7 +74,8 @@ public class CosmosAdmin implements DistributedStorageAdmin {
"cosmos",
StorageInfo.MutationAtomicityUnit.PARTITION,
// No limit on the number of mutations
- Integer.MAX_VALUE);
+ Integer.MAX_VALUE,
+ false);
private final CosmosClient client;
private final String metadataDatabase;
diff --git a/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java b/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java
index fc420242db..ceecf593fe 100644
--- a/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java
+++ b/core/src/main/java/com/scalar/db/storage/dynamo/DynamoAdmin.java
@@ -165,7 +165,8 @@ public class DynamoAdmin implements DistributedStorageAdmin {
"dynamo",
StorageInfo.MutationAtomicityUnit.STORAGE,
// DynamoDB has a limit of 100 items per transactional batch write operation
- 100);
+ 100,
+ false);
private final DynamoDbClient client;
private final ApplicationAutoScalingClient applicationAutoScalingClient;
diff --git a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java
index 4a1d505f4d..3085ca629c 100644
--- a/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java
+++ b/core/src/main/java/com/scalar/db/storage/jdbc/JdbcAdmin.java
@@ -53,12 +53,6 @@ public class JdbcAdmin implements DistributedStorageAdmin {
@VisibleForTesting static final String JDBC_COL_DECIMAL_DIGITS = "DECIMAL_DIGITS";
private static final String INDEX_NAME_PREFIX = "index";
- private static final StorageInfo STORAGE_INFO =
- new StorageInfoImpl(
- "jdbc",
- StorageInfo.MutationAtomicityUnit.STORAGE,
- // No limit on the number of mutations
- Integer.MAX_VALUE);
private final RdbEngineStrategy rdbEngine;
private final BasicDataSource dataSource;
@@ -1011,8 +1005,22 @@ public void upgrade(Map A virtual table read involves querying multiple underlying source tables. When using a lower
+ * isolation level, there is a risk of observing an inconsistent snapshot where data from
+ * different source tables reflects different points in time. This method returns the minimum
+ * isolation level that guarantees a consistent snapshot across all source tables involved in a
+ * virtual table read.
+ *
+ * @return the minimum isolation level required for consistent virtual table reads
+ */
+ int getMinimumIsolationLevelForConsistentVirtualTableRead();
}
diff --git a/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java b/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java
index 0d19989f63..0a5638e233 100644
--- a/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java
+++ b/core/src/main/java/com/scalar/db/storage/multistorage/MultiStorageAdmin.java
@@ -305,7 +305,8 @@ public StorageInfo getStorageInfo(String namespace) throws ExecutionException {
return new StorageInfoImpl(
holder.storageName,
storageInfo.getMutationAtomicityUnit(),
- storageInfo.getMaxAtomicMutationsCount());
+ storageInfo.getMaxAtomicMutationsCount(),
+ storageInfo.isConsistentVirtualTableReadGuaranteed());
} catch (RuntimeException e) {
if (e.getCause() instanceof ExecutionException) {
throw (ExecutionException) e.getCause();
diff --git a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java
index 681e6fcf2a..8c68e3cc44 100644
--- a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java
+++ b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageAdmin.java
@@ -37,7 +37,8 @@ public class ObjectStorageAdmin implements DistributedStorageAdmin {
"object_storage",
StorageInfo.MutationAtomicityUnit.PARTITION,
// No limit on the number of mutations
- Integer.MAX_VALUE);
+ Integer.MAX_VALUE,
+ false);
private final ObjectStorageWrapper wrapper;
private final String metadataNamespace;
diff --git a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java
index 96e4117c41..8a5c2803d1 100644
--- a/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java
+++ b/core/src/main/java/com/scalar/db/storage/objectstorage/ObjectStorageWrapper.java
@@ -76,6 +76,10 @@ public interface ObjectStorageWrapper {
*/
void deleteByPrefix(String prefix) throws ObjectStorageWrapperException;
- /** Close the storage wrapper. */
+ /**
+ * Close the storage wrapper.
+ *
+ * @throws ObjectStorageWrapperException if an error occurs
+ */
void close() throws ObjectStorageWrapperException;
}
diff --git a/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java b/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java
index 9e6fd87310..ec6a6833fb 100644
--- a/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java
+++ b/core/src/test/java/com/scalar/db/common/checker/OperationCheckerTest.java
@@ -53,7 +53,7 @@ public class OperationCheckerTest {
private static final String COL3 = "v3";
private static final StorageInfo STORAGE_INFO =
new StorageInfoImpl(
- "cassandra", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE);
+ "cassandra", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE, false);
@Mock private DatabaseConfig databaseConfig;
@Mock private TableMetadataManager metadataManager;
@@ -2059,9 +2059,11 @@ public void check_MutationsGiven_ForAtomicityUnit_ShouldBehaveCorrectly(
.addClusteringKey(CKEY1)
.build());
- StorageInfo storageInfo1 = new StorageInfoImpl("s1", mutationAtomicityUnit, Integer.MAX_VALUE);
+ StorageInfo storageInfo1 =
+ new StorageInfoImpl("s1", mutationAtomicityUnit, Integer.MAX_VALUE, false);
StorageInfo storageInfo2 =
- new StorageInfoImpl("s2", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE);
+ new StorageInfoImpl(
+ "s2", StorageInfo.MutationAtomicityUnit.STORAGE, Integer.MAX_VALUE, false);
when(storageInfoProvider.getStorageInfo("ns")).thenReturn(storageInfo1);
when(storageInfoProvider.getStorageInfo("ns2")).thenReturn(storageInfo1);
when(storageInfoProvider.getStorageInfo("other_ns")).thenReturn(storageInfo2);
diff --git a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java
index b11dcfb782..7265b74424 100644
--- a/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java
+++ b/core/src/test/java/com/scalar/db/storage/cosmos/CosmosOperationCheckerTest.java
@@ -41,7 +41,8 @@ public class CosmosOperationCheckerTest {
private static final String COL1 = "v1";
private static final String COL2 = "v2";
private static final StorageInfo STORAGE_INFO =
- new StorageInfoImpl("cosmos", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE);
+ new StorageInfoImpl(
+ "cosmos", StorageInfo.MutationAtomicityUnit.PARTITION, Integer.MAX_VALUE, false);
private static final TableMetadata TABLE_METADATA1 =
TableMetadata.newBuilder()
diff --git a/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java b/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java
index 59e2fbad47..b2e91b9dfa 100644
--- a/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java
+++ b/core/src/test/java/com/scalar/db/storage/dynamo/DynamoOperationCheckerTest.java
@@ -39,7 +39,7 @@ public class DynamoOperationCheckerTest {
private static final String COL3 = "v3";
private static final String COL4 = "v4";
private static final StorageInfo STORAGE_INFO =
- new StorageInfoImpl("dynamo", StorageInfo.MutationAtomicityUnit.STORAGE, 100);
+ new StorageInfoImpl("dynamo", StorageInfo.MutationAtomicityUnit.STORAGE, 100, false);
@Mock private DatabaseConfig databaseConfig;
@Mock private TableMetadataManager metadataManager;
diff --git a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java
index db08667898..99eff786c2 100644
--- a/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java
+++ b/core/src/test/java/com/scalar/db/storage/jdbc/JdbcAdminTest.java
@@ -35,6 +35,7 @@
import com.google.common.collect.ImmutableSet;
import com.mysql.cj.jdbc.exceptions.CommunicationsException;
import com.scalar.db.api.Scan.Ordering.Order;
+import com.scalar.db.api.StorageInfo;
import com.scalar.db.api.TableMetadata;
import com.scalar.db.api.VirtualTableInfo;
import com.scalar.db.api.VirtualTableJoinType;
@@ -6199,6 +6200,103 @@ public void getVirtualTableInfo_SQLExceptionThrown_ShouldThrowExecutionException
.hasCause(sqlException);
}
+ @ParameterizedTest
+ @EnumSource(RdbEngine.class)
+ void getStorageInfo_WithRepeatableReadIsolationLevel_ShouldReturnCorrectInfo(RdbEngine rdbEngine)
+ throws Exception {
+ // Arrange
+ int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ;
+ JdbcAdmin admin = createJdbcAdminFor(rdbEngine);
+ when(dataSource.getConnection()).thenReturn(connection);
+ when(connection.getTransactionIsolation()).thenReturn(isolationLevel);
+
+ // Act
+ StorageInfo storageInfo = admin.getStorageInfo("namespace");
+
+ // Assert
+ assertThat(storageInfo.getStorageName()).isEqualTo("jdbc");
+ assertThat(storageInfo.getMutationAtomicityUnit())
+ .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE);
+ assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE);
+
+ // Check consistent virtual table read guarantee based on RDB engine
+ RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine);
+ boolean expectedConsistentVirtualTableReadGuaranteed =
+ isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead();
+ assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed())
+ .isEqualTo(expectedConsistentVirtualTableReadGuaranteed);
+ }
+
+ @ParameterizedTest
+ @EnumSource(RdbEngine.class)
+ void getStorageInfo_WithReadCommittedIsolationLevel_ShouldReturnCorrectInfo(RdbEngine rdbEngine)
+ throws Exception {
+ // Arrange
+ int isolationLevel = Connection.TRANSACTION_READ_COMMITTED;
+ JdbcAdmin admin = createJdbcAdminFor(rdbEngine);
+ when(dataSource.getConnection()).thenReturn(connection);
+ when(connection.getTransactionIsolation()).thenReturn(isolationLevel);
+
+ // Act
+ StorageInfo storageInfo = admin.getStorageInfo("namespace");
+
+ // Assert
+ assertThat(storageInfo.getStorageName()).isEqualTo("jdbc");
+ assertThat(storageInfo.getMutationAtomicityUnit())
+ .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE);
+ assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE);
+
+ // Check consistent virtual table read guarantee based on RDB engine
+ RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine);
+ boolean expectedConsistentVirtualTableReadGuaranteed =
+ isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead();
+ assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed())
+ .isEqualTo(expectedConsistentVirtualTableReadGuaranteed);
+ }
+
+ @ParameterizedTest
+ @EnumSource(RdbEngine.class)
+ void getStorageInfo_WithSerializableIsolationLevel_ShouldReturnCorrectInfo(RdbEngine rdbEngine)
+ throws Exception {
+ // Arrange
+ int isolationLevel = Connection.TRANSACTION_SERIALIZABLE;
+ JdbcAdmin admin = createJdbcAdminFor(rdbEngine);
+ when(dataSource.getConnection()).thenReturn(connection);
+ when(connection.getTransactionIsolation()).thenReturn(isolationLevel);
+
+ // Act
+ StorageInfo storageInfo = admin.getStorageInfo("namespace");
+
+ // Assert
+ assertThat(storageInfo.getStorageName()).isEqualTo("jdbc");
+ assertThat(storageInfo.getMutationAtomicityUnit())
+ .isEqualTo(StorageInfo.MutationAtomicityUnit.STORAGE);
+ assertThat(storageInfo.getMaxAtomicMutationsCount()).isEqualTo(Integer.MAX_VALUE);
+
+ // Check consistent virtual table read guarantee based on RDB engine
+ RdbEngineStrategy strategy = getRdbEngineStrategy(rdbEngine);
+ boolean expectedConsistentVirtualTableReadGuaranteed =
+ isolationLevel >= strategy.getMinimumIsolationLevelForConsistentVirtualTableRead();
+ assertThat(storageInfo.isConsistentVirtualTableReadGuaranteed())
+ .isEqualTo(expectedConsistentVirtualTableReadGuaranteed);
+ }
+
+ @ParameterizedTest
+ @EnumSource(RdbEngine.class)
+ void getStorageInfo_SQLExceptionThrown_ShouldThrowExecutionException(RdbEngine rdbEngine)
+ throws Exception {
+ // Arrange
+ JdbcAdmin admin = createJdbcAdminFor(rdbEngine);
+ when(dataSource.getConnection()).thenReturn(connection);
+ when(connection.getTransactionIsolation()).thenThrow(new SQLException("Connection error"));
+
+ // Act Assert
+ assertThatThrownBy(() -> admin.getStorageInfo("namespace"))
+ .isInstanceOf(ExecutionException.class)
+ .hasMessageContaining("Getting the transaction isolation level failed")
+ .hasCauseInstanceOf(SQLException.class);
+ }
+
// Utility class used to mock ResultSet for a "select * from" query on the metadata table
static class SelectAllFromMetadataTableResultSetMocker
implements org.mockito.stubbing.Answer