diff --git a/build.gradle b/build.gradle index bd99888733..ccd395cbd2 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ subprojects { googleCloudStorageVersion = '2.60.0' picocliVersion = '4.7.7' commonsTextVersion = '1.14.0' + caffeineVersion = '2.9.3' junitVersion = '5.14.1' commonsLangVersion = '3.20.0' assertjVersion = '3.27.6' diff --git a/core/build.gradle b/core/build.gradle index 0bd71ae5f5..55ccf68809 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -197,6 +197,7 @@ dependencies { exclude group: 'org.slf4j', module: 'slf4j-api' } implementation "org.apache.commons:commons-text:${commonsTextVersion}" + implementation "com.github.ben-manes.caffeine:caffeine:${caffeineVersion}" testImplementation platform("org.junit:junit-bom:${junitVersion}") testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/core/src/main/java/com/scalar/db/common/TableMetadataManager.java b/core/src/main/java/com/scalar/db/common/TableMetadataManager.java index 264a8904a9..4c479f24ca 100644 --- a/core/src/main/java/com/scalar/db/common/TableMetadataManager.java +++ b/core/src/main/java/com/scalar/db/common/TableMetadataManager.java @@ -1,8 +1,7 @@ package com.scalar.db.common; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; import com.scalar.db.api.Admin; import com.scalar.db.api.Operation; import com.scalar.db.api.TableMetadata; @@ -10,39 +9,29 @@ import com.scalar.db.util.ScalarDbUtils; import com.scalar.db.util.ThrowableFunction; import java.util.Objects; -import java.util.Optional; +import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; /** A class that manages and caches table metadata */ @ThreadSafe public class TableMetadataManager { - private final LoadingCache> tableMetadataCache; + private final LoadingCache tableMetadataCache; public TableMetadataManager(Admin admin, long cacheExpirationTimeSecs) { - this( - key -> Optional.ofNullable(admin.getTableMetadata(key.namespace, key.table)), - cacheExpirationTimeSecs); + this(key -> admin.getTableMetadata(key.namespace, key.table), cacheExpirationTimeSecs); } public TableMetadataManager( - ThrowableFunction, Exception> getTableMetadataFunc, + ThrowableFunction getTableMetadataFunc, long cacheExpirationTimeSecs) { - CacheBuilder builder = CacheBuilder.newBuilder(); + Caffeine builder = Caffeine.newBuilder(); if (cacheExpirationTimeSecs >= 0) { builder.expireAfterWrite(cacheExpirationTimeSecs, TimeUnit.SECONDS); } - tableMetadataCache = - builder.build( - new CacheLoader>() { - @Nonnull - @Override - public Optional load(@Nonnull TableKey key) throws Exception { - return getTableMetadataFunc.apply(key); - } - }); + tableMetadataCache = builder.build(getTableMetadataFunc::apply); } /** @@ -61,22 +50,23 @@ public TableMetadata getTableMetadata(Operation operation) throws ExecutionExcep } /** - * Returns a table metadata corresponding to the specified operation. + * Returns a table metadata corresponding to the specified namespace and table. * * @param namespace a namespace to retrieve * @param table a table to retrieve * @return a table metadata. null if the table is not found. * @throws ExecutionException if the operation fails */ + @Nullable public TableMetadata getTableMetadata(String namespace, String table) throws ExecutionException { try { TableKey key = new TableKey(namespace, table); - return tableMetadataCache.get(key).orElse(null); - } catch (java.util.concurrent.ExecutionException e) { + return tableMetadataCache.get(key); + } catch (CompletionException e) { throw new ExecutionException( CoreError.GETTING_TABLE_METADATA_FAILED.buildMessage( ScalarDbUtils.getFullTableName(namespace, table)), - e); + e.getCause()); } } diff --git a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManager.java b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManager.java index 1f1c9fb3a9..d2b4c7569f 100644 --- a/core/src/main/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManager.java +++ b/core/src/main/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManager.java @@ -1,8 +1,7 @@ package com.scalar.db.transaction.consensuscommit; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.Operation; import com.scalar.db.api.TableMetadata; @@ -10,37 +9,31 @@ import com.scalar.db.exception.storage.ExecutionException; import com.scalar.db.util.ScalarDbUtils; import java.util.Objects; -import java.util.Optional; +import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe public class TransactionTableMetadataManager { - private final LoadingCache> tableMetadataCache; + private final LoadingCache tableMetadataCache; public TransactionTableMetadataManager( DistributedStorageAdmin admin, long cacheExpirationTimeSecs) { - CacheBuilder builder = CacheBuilder.newBuilder(); + Caffeine builder = Caffeine.newBuilder(); if (cacheExpirationTimeSecs >= 0) { builder.expireAfterWrite(cacheExpirationTimeSecs, TimeUnit.SECONDS); } tableMetadataCache = builder.build( - new CacheLoader>() { - @Nonnull - @Override - public Optional load(@Nonnull TableKey key) - throws ExecutionException { - TableMetadata tableMetadata = admin.getTableMetadata(key.namespace, key.table); - if (tableMetadata == null) { - return Optional.empty(); - } - return Optional.of(new TransactionTableMetadata(tableMetadata)); + key -> { + TableMetadata tableMetadata = admin.getTableMetadata(key.namespace, key.table); + if (tableMetadata == null) { + return null; } + return new TransactionTableMetadata(tableMetadata); }); } @@ -74,12 +67,12 @@ public TransactionTableMetadata getTransactionTableMetadata(String namespace, St throws ExecutionException { try { TableKey key = new TableKey(namespace, table); - return tableMetadataCache.get(key).orElse(null); - } catch (java.util.concurrent.ExecutionException e) { + return tableMetadataCache.get(key); + } catch (CompletionException e) { throw new ExecutionException( CoreError.GETTING_TABLE_METADATA_FAILED.buildMessage( ScalarDbUtils.getFullTableName(namespace, table)), - e); + e.getCause()); } } diff --git a/core/src/test/java/com/scalar/db/common/TableMetadataManagerTest.java b/core/src/test/java/com/scalar/db/common/TableMetadataManagerTest.java index 9c6d01f0d6..3290014462 100644 --- a/core/src/test/java/com/scalar/db/common/TableMetadataManagerTest.java +++ b/core/src/test/java/com/scalar/db/common/TableMetadataManagerTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -14,7 +15,6 @@ import com.scalar.db.io.DataType; import com.scalar.db.io.Key; import com.scalar.db.util.ThrowableFunction; -import java.util.Optional; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,7 +26,7 @@ public class TableMetadataManagerTest { @Mock private DistributedStorageAdmin admin; @Mock - private ThrowableFunction, Exception> + private ThrowableFunction getTableMetadataFunc; @BeforeEach @@ -116,20 +116,80 @@ public void getTableMetadata_CalledAfterCacheExpiration_ShouldCallDistributedSto assertThat(actualTableMetadata).isEqualTo(expectedTableMetadata); } + @Test + public void getTableMetadata_TableNotFound_ShouldReturnNull() throws ExecutionException { + // Arrange + TableMetadataManager tableMetadataManager = new TableMetadataManager(admin, -1); + + when(admin.getTableMetadata("ns", "tbl")).thenReturn(null); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + TableMetadata actualTableMetadata = tableMetadataManager.getTableMetadata(get); + + // Assert + verify(admin).getTableMetadata("ns", "tbl"); + assertThat(actualTableMetadata).isNull(); + } + + @Test + public void getTableMetadata_TableNotFoundCalledTwice_ShouldCallDistributedStorageAdminTwice() + throws ExecutionException { + // Arrange + TableMetadataManager tableMetadataManager = new TableMetadataManager(admin, -1); + + when(admin.getTableMetadata("ns", "tbl")).thenReturn(null); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + tableMetadataManager.getTableMetadata(get); + TableMetadata actualTableMetadata = tableMetadataManager.getTableMetadata(get); + + // Assert + verify(admin, times(2)).getTableMetadata("ns", "tbl"); + assertThat(actualTableMetadata).isNull(); + } + @Test public void getTableMetadata_ExecutionExceptionThrownByAdmin_ShouldThrowExecutionException() throws ExecutionException { // Arrange TableMetadataManager tableMetadataManager = new TableMetadataManager(admin, 1L); // one second - when(admin.getTableMetadata("ns", "tbl")).thenThrow(ExecutionException.class); + ExecutionException expectedException = mock(ExecutionException.class); + when(admin.getTableMetadata("ns", "tbl")).thenThrow(expectedException); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + assertThatThrownBy(() -> tableMetadataManager.getTableMetadata(get)) + .isInstanceOf(ExecutionException.class) + .hasCause(expectedException); + + // Assert + verify(admin).getTableMetadata("ns", "tbl"); + } + + @Test + public void getTableMetadata_RuntimeExceptionThrownByAdmin_ShouldThrowRuntimeException() + throws ExecutionException { + // Arrange + TableMetadataManager tableMetadataManager = new TableMetadataManager(admin, 1L); // one second + + IllegalArgumentException illegalArgumentException = mock(IllegalArgumentException.class); + when(admin.getTableMetadata("ns", "tbl")).thenThrow(illegalArgumentException); Get get = Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); // Act assertThatThrownBy(() -> tableMetadataManager.getTableMetadata(get)) - .isInstanceOf(ExecutionException.class); + .isEqualTo(illegalArgumentException); // Assert verify(admin).getTableMetadata("ns", "tbl"); @@ -150,7 +210,7 @@ public void getTableMetadata_ExecutionExceptionThrownByAdmin_ShouldThrowExecutio .build(); when(getTableMetadataFunc.apply(new TableMetadataManager.TableKey("ns", "tbl"))) - .thenReturn(Optional.of(expectedTableMetadata)); + .thenReturn(expectedTableMetadata); Get get = Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); @@ -178,7 +238,7 @@ public void getTableMetadata_ExecutionExceptionThrownByAdmin_ShouldThrowExecutio .build(); when(getTableMetadataFunc.apply(new TableMetadataManager.TableKey("ns", "tbl"))) - .thenReturn(Optional.of(expectedTableMetadata)); + .thenReturn(expectedTableMetadata); Get get = Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); @@ -208,7 +268,7 @@ public void getTableMetadata_ExecutionExceptionThrownByAdmin_ShouldThrowExecutio .build(); when(getTableMetadataFunc.apply(new TableMetadataManager.TableKey("ns", "tbl"))) - .thenReturn(Optional.of(expectedTableMetadata)); + .thenReturn(expectedTableMetadata); Get get = Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); @@ -224,6 +284,48 @@ public void getTableMetadata_ExecutionExceptionThrownByAdmin_ShouldThrowExecutio assertThat(actualTableMetadata).isEqualTo(expectedTableMetadata); } + @Test + public void getTableMetadata_WithGetTableMetadataFunc_TableNotFound_ShouldReturnNull() + throws Exception { + // Arrange + TableMetadataManager tableMetadataManager = new TableMetadataManager(getTableMetadataFunc, -1); + + when(getTableMetadataFunc.apply(new TableMetadataManager.TableKey("ns", "tbl"))) + .thenReturn(null); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + TableMetadata actualTableMetadata = tableMetadataManager.getTableMetadata(get); + + // Assert + verify(getTableMetadataFunc).apply(new TableMetadataManager.TableKey("ns", "tbl")); + assertThat(actualTableMetadata).isNull(); + } + + @Test + public void + getTableMetadata_WithGetTableMetadataFunc_TableNotFoundCalledTwice_ShouldCallGetTableMetadataFuncTwice() + throws Exception { + // Arrange + TableMetadataManager tableMetadataManager = new TableMetadataManager(getTableMetadataFunc, -1); + + when(getTableMetadataFunc.apply(new TableMetadataManager.TableKey("ns", "tbl"))) + .thenReturn(null); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + tableMetadataManager.getTableMetadata(get); + TableMetadata actualTableMetadata = tableMetadataManager.getTableMetadata(get); + + // Assert + verify(getTableMetadataFunc, times(2)).apply(new TableMetadataManager.TableKey("ns", "tbl")); + assertThat(actualTableMetadata).isNull(); + } + @Test public void getTableMetadata_ExceptionThrownByGetTableMetadataFunc_ShouldThrowExecutionException() throws Exception { @@ -231,15 +333,40 @@ public void getTableMetadata_ExceptionThrownByGetTableMetadataFunc_ShouldThrowEx TableMetadataManager tableMetadataManager = new TableMetadataManager(getTableMetadataFunc, 1L); // one second + ExecutionException expectedException = mock(ExecutionException.class); + when(getTableMetadataFunc.apply(new TableMetadataManager.TableKey("ns", "tbl"))) + .thenThrow(expectedException); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + assertThatThrownBy(() -> tableMetadataManager.getTableMetadata(get)) + .isInstanceOf(ExecutionException.class) + .hasCause(expectedException); + + // Assert + verify(getTableMetadataFunc).apply(new TableMetadataManager.TableKey("ns", "tbl")); + } + + @Test + public void + getTableMetadata_RuntimeExceptionThrownByGetTableMetadataFunc_ShouldThrowRuntimeException() + throws Exception { + // Arrange + TableMetadataManager tableMetadataManager = + new TableMetadataManager(getTableMetadataFunc, 1L); // one second + + IllegalArgumentException illegalArgumentException = mock(IllegalArgumentException.class); when(getTableMetadataFunc.apply(new TableMetadataManager.TableKey("ns", "tbl"))) - .thenThrow(Exception.class); + .thenThrow(illegalArgumentException); Get get = Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); // Act assertThatThrownBy(() -> tableMetadataManager.getTableMetadata(get)) - .isInstanceOf(ExecutionException.class); + .isEqualTo(illegalArgumentException); // Assert verify(getTableMetadataFunc).apply(new TableMetadataManager.TableKey("ns", "tbl")); diff --git a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManagerTest.java b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManagerTest.java index 1df39fc38b..57a3cdd4d5 100644 --- a/core/src/test/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManagerTest.java +++ b/core/src/test/java/com/scalar/db/transaction/consensuscommit/TransactionTableMetadataManagerTest.java @@ -1,7 +1,9 @@ package com.scalar.db.transaction.consensuscommit; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -126,7 +128,95 @@ public void getTransactionTableMetadata_CalledTwice_ShouldCallDistributedStorage assertTransactionMetadata(actual); } + @Test + public void getTransactionTableMetadata_TableNotFound_ShouldReturnNull() + throws ExecutionException { + // Arrange + TransactionTableMetadataManager tableMetadataManager = + new TransactionTableMetadataManager(admin, -1); + + when(admin.getTableMetadata("ns", "tbl")).thenReturn(null); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + TransactionTableMetadata actual = tableMetadataManager.getTransactionTableMetadata(get); + + // Assert + verify(admin).getTableMetadata("ns", "tbl"); + assertThat(actual).isNull(); + } + + @Test + public void + getTransactionTableMetadata_TableNotFoundCalledTwice_ShouldCallDistributedStorageAdminTwice() + throws ExecutionException { + // Arrange + TransactionTableMetadataManager tableMetadataManager = + new TransactionTableMetadataManager(admin, -1); + + when(admin.getTableMetadata("ns", "tbl")).thenReturn(null); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + tableMetadataManager.getTransactionTableMetadata(get); + TransactionTableMetadata actual = tableMetadataManager.getTransactionTableMetadata(get); + + // Assert + verify(admin, times(2)).getTableMetadata("ns", "tbl"); + assertThat(actual).isNull(); + } + + @Test + public void + getTransactionTableMetadata_ExecutionExceptionThrownByAdmin_ShouldThrowExecutionException() + throws ExecutionException { + // Arrange + TransactionTableMetadataManager tableMetadataManager = + new TransactionTableMetadataManager(admin, -1); + + ExecutionException expectedException = mock(ExecutionException.class); + when(admin.getTableMetadata("ns", "tbl")).thenThrow(expectedException); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + assertThatThrownBy(() -> tableMetadataManager.getTransactionTableMetadata(get)) + .isInstanceOf(ExecutionException.class) + .hasCause(expectedException); + + // Assert + verify(admin).getTableMetadata("ns", "tbl"); + } + + @Test + public void + getTransactionTableMetadata_RuntimeExceptionThrownByAdmin_ShouldThrowRuntimeException() + throws ExecutionException { + // Arrange + TransactionTableMetadataManager tableMetadataManager = + new TransactionTableMetadataManager(admin, -1); + + IllegalArgumentException illegalArgumentException = mock(IllegalArgumentException.class); + when(admin.getTableMetadata("ns", "tbl")).thenThrow(illegalArgumentException); + + Get get = + Get.newBuilder().namespace("ns").table("tbl").partitionKey(Key.ofText("c1", "aaa")).build(); + + // Act + assertThatThrownBy(() -> tableMetadataManager.getTransactionTableMetadata(get)) + .isEqualTo(illegalArgumentException); + + // Assert + verify(admin).getTableMetadata("ns", "tbl"); + } + private void assertTransactionMetadata(TransactionTableMetadata actual) { + assertThat(actual).isNotNull(); assertThat(actual.getTableMetadata()).isEqualTo(tableMetadata); assertThat(actual.getPartitionKeyNames()) .isEqualTo(new LinkedHashSet<>(Collections.singletonList(ACCOUNT_ID)));