diff --git a/core/src/main/java/com/scalar/db/config/DatabaseConfig.java b/core/src/main/java/com/scalar/db/config/DatabaseConfig.java index c448362d8b..488f3bf8a8 100644 --- a/core/src/main/java/com/scalar/db/config/DatabaseConfig.java +++ b/core/src/main/java/com/scalar/db/config/DatabaseConfig.java @@ -51,6 +51,8 @@ public class DatabaseConfig { private Class transactionManagerClass; private Class twoPhaseCommitTransactionManagerClass; private Isolation isolation = Isolation.SNAPSHOT; + private long tableMetadataCacheExpirationTimeSecs = -1; + public static final String PREFIX = "scalar.db."; public static final String CONTACT_POINTS = PREFIX + "contact_points"; public static final String CONTACT_PORT = PREFIX + "contact_port"; @@ -60,6 +62,8 @@ public class DatabaseConfig { public static final String NAMESPACE_PREFIX = PREFIX + "namespace_prefix"; public static final String TRANSACTION_MANAGER = PREFIX + "transaction_manager"; public static final String ISOLATION_LEVEL = PREFIX + "isolation_level"; + public static final String TABLE_METADATA_CACHE_EXPIRATION_TIME_SECS = + PREFIX + "table_metadata.cache_expiration_time_secs"; public DatabaseConfig(File propertiesFile) throws IOException { this(new FileInputStream(propertiesFile)); @@ -182,6 +186,11 @@ protected void load() { if (!Strings.isNullOrEmpty(props.getProperty(ISOLATION_LEVEL))) { isolation = Isolation.valueOf(props.getProperty(ISOLATION_LEVEL).toUpperCase()); } + + if (!Strings.isNullOrEmpty(props.getProperty(TABLE_METADATA_CACHE_EXPIRATION_TIME_SECS))) { + tableMetadataCacheExpirationTimeSecs = + Long.parseLong(props.getProperty(TABLE_METADATA_CACHE_EXPIRATION_TIME_SECS)); + } } public List getContactPoints() { @@ -224,4 +233,8 @@ public Class getTransactionManagerClass public Isolation getIsolation() { return isolation; } + + public long getTableMetadataCacheExpirationTimeSecs() { + return tableMetadataCacheExpirationTimeSecs; + } } diff --git a/core/src/main/java/com/scalar/db/storage/common/TableMetadataManager.java b/core/src/main/java/com/scalar/db/storage/common/TableMetadataManager.java index e0b935b53d..04548ce716 100644 --- a/core/src/main/java/com/scalar/db/storage/common/TableMetadataManager.java +++ b/core/src/main/java/com/scalar/db/storage/common/TableMetadataManager.java @@ -10,6 +10,7 @@ import com.scalar.db.exception.storage.ExecutionException; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; /** A class that manages and caches table metadata */ @@ -18,16 +19,19 @@ public class TableMetadataManager { private final LoadingCache> tableMetadataCache; public TableMetadataManager(DistributedStorageAdmin admin, DatabaseConfig config) { + CacheBuilder builder = CacheBuilder.newBuilder(); + long tableMetadataCacheExpirationTimeSecs = config.getTableMetadataCacheExpirationTimeSecs(); + if (tableMetadataCacheExpirationTimeSecs > 0) { + builder.expireAfterWrite(tableMetadataCacheExpirationTimeSecs, TimeUnit.SECONDS); + } tableMetadataCache = - CacheBuilder.newBuilder() - .build( - new CacheLoader>() { - @Override - public Optional load(@Nonnull TableKey key) - throws ExecutionException { - return Optional.ofNullable(admin.getTableMetadata(key.namespace, key.table)); - } - }); + builder.build( + new CacheLoader>() { + @Override + public Optional load(@Nonnull TableKey key) throws ExecutionException { + return Optional.ofNullable(admin.getTableMetadata(key.namespace, key.table)); + } + }); } /** diff --git a/core/src/test/java/com/scalar/db/config/DatabaseConfigTest.java b/core/src/test/java/com/scalar/db/config/DatabaseConfigTest.java index 303d8c6ba5..c62588a424 100644 --- a/core/src/test/java/com/scalar/db/config/DatabaseConfigTest.java +++ b/core/src/test/java/com/scalar/db/config/DatabaseConfigTest.java @@ -52,6 +52,7 @@ public void constructor_PropertiesWithoutPortGiven_ShouldLoadProperly() { assertThat(config.getNamespacePrefix()).isNotPresent(); assertThat(config.getTransactionManagerClass()).isEqualTo(ConsensusCommitManager.class); assertThat(config.getIsolation()).isEqualTo(Isolation.SNAPSHOT); + assertThat(config.getTableMetadataCacheExpirationTimeSecs()).isEqualTo(-1); } @Test @@ -76,6 +77,7 @@ public void constructor_PropertiesWithoutUsernameGiven_ShouldLoadProperly() { assertThat(config.getNamespacePrefix()).isNotPresent(); assertThat(config.getTransactionManagerClass()).isEqualTo(ConsensusCommitManager.class); assertThat(config.getIsolation()).isEqualTo(Isolation.SNAPSHOT); + assertThat(config.getTableMetadataCacheExpirationTimeSecs()).isEqualTo(-1); } @Test @@ -100,6 +102,7 @@ public void constructor_PropertiesWithoutPasswordGiven_ShouldLoadProperly() { assertThat(config.getNamespacePrefix()).isNotPresent(); assertThat(config.getTransactionManagerClass()).isEqualTo(ConsensusCommitManager.class); assertThat(config.getIsolation()).isEqualTo(Isolation.SNAPSHOT); + assertThat(config.getTableMetadataCacheExpirationTimeSecs()).isEqualTo(-1); } @Test @@ -126,6 +129,7 @@ public void constructor_PropertiesWithPortGiven_ShouldLoadProperly() { assertThat(config.getNamespacePrefix()).isNotPresent(); assertThat(config.getTransactionManagerClass()).isEqualTo(ConsensusCommitManager.class); assertThat(config.getIsolation()).isEqualTo(Isolation.SNAPSHOT); + assertThat(config.getTableMetadataCacheExpirationTimeSecs()).isEqualTo(-1); } @Test @@ -442,4 +446,26 @@ public void constructor_UnsupportedIsolationGiven_ShouldThrowIllegalArgumentExce assertThatThrownBy(() -> new DatabaseConfig(props)) .isInstanceOf(IllegalArgumentException.class); } + + @Test + public void constructor_PropertiesWithTableMetadataExpirationTimeSecsGiven_ShouldLoadProperly() { + // Arrange + Properties props = new Properties(); + props.setProperty(DatabaseConfig.CONTACT_POINTS, ANY_HOST); + props.setProperty(DatabaseConfig.USERNAME, ANY_USERNAME); + props.setProperty(DatabaseConfig.PASSWORD, ANY_PASSWORD); + props.setProperty(DatabaseConfig.TABLE_METADATA_CACHE_EXPIRATION_TIME_SECS, "3600"); + + // Act + DatabaseConfig config = new DatabaseConfig(props); + + // Assert + assertThat(config.getContactPoints()).isEqualTo(Collections.singletonList(ANY_HOST)); + assertThat(config.getContactPort()).isEqualTo(0); + assertThat(config.getUsername().isPresent()).isTrue(); + assertThat(config.getUsername().get()).isEqualTo(ANY_USERNAME); + assertThat(config.getPassword().isPresent()).isTrue(); + assertThat(config.getPassword().get()).isEqualTo(ANY_PASSWORD); + assertThat(config.getTableMetadataCacheExpirationTimeSecs()).isEqualTo(3600); + } } diff --git a/core/src/test/java/com/scalar/db/storage/common/TableMetadataManagerTest.java b/core/src/test/java/com/scalar/db/storage/common/TableMetadataManagerTest.java index e4322c24d4..8d11364ccc 100644 --- a/core/src/test/java/com/scalar/db/storage/common/TableMetadataManagerTest.java +++ b/core/src/test/java/com/scalar/db/storage/common/TableMetadataManagerTest.java @@ -2,9 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.common.util.concurrent.Uninterruptibles; import com.scalar.db.api.DistributedStorageAdmin; import com.scalar.db.api.Get; import com.scalar.db.api.TableMetadata; @@ -13,6 +15,7 @@ import com.scalar.db.io.DataType; import com.scalar.db.io.Key; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -33,6 +36,7 @@ public void setUp() { public void getTableMetadata_CalledOnce_ShouldCallDistributedStorageAdminOnce() throws ExecutionException { // Arrange + when(config.getTableMetadataCacheExpirationTimeSecs()).thenReturn(-1L); TableMetadataManager tableMetadataManager = new TableMetadataManager(admin, config); TableMetadata expectedTableMetadata = @@ -50,6 +54,7 @@ public void getTableMetadata_CalledOnce_ShouldCallDistributedStorageAdminOnce() new Get(new Key("c1", "aaa")).forNamespace("ns").forTable("tbl")); // Assert + verify(config).getTableMetadataCacheExpirationTimeSecs(); verify(admin).getTableMetadata(anyString(), anyString()); assertThat(actualTableMetadata).isEqualTo(expectedTableMetadata); } @@ -58,6 +63,7 @@ public void getTableMetadata_CalledOnce_ShouldCallDistributedStorageAdminOnce() public void getTableMetadata_CalledTwice_ShouldCallDistributedStorageAdminOnlyOnce() throws ExecutionException { // Arrange + when(config.getTableMetadataCacheExpirationTimeSecs()).thenReturn(-1L); TableMetadataManager tableMetadataManager = new TableMetadataManager(admin, config); TableMetadata expectedTableMetadata = @@ -76,7 +82,38 @@ public void getTableMetadata_CalledTwice_ShouldCallDistributedStorageAdminOnlyOn TableMetadata actualTableMetadata = tableMetadataManager.getTableMetadata(get); // Assert + verify(config).getTableMetadataCacheExpirationTimeSecs(); verify(admin).getTableMetadata(anyString(), anyString()); assertThat(actualTableMetadata).isEqualTo(expectedTableMetadata); } + + @Test + public void getTableMetadata_CalledAfterCacheExpiration_ShouldCallDistributedStorageAdminAgain() + throws ExecutionException { + // Arrange + when(config.getTableMetadataCacheExpirationTimeSecs()).thenReturn(1L); // one second + TableMetadataManager tableMetadataManager = new TableMetadataManager(admin, config); + + TableMetadata expectedTableMetadata = + TableMetadata.newBuilder() + .addColumn("c1", DataType.INT) + .addColumn("c2", DataType.INT) + .addPartitionKey("c1") + .build(); + + when(admin.getTableMetadata(anyString(), anyString())).thenReturn(expectedTableMetadata); + + Get get = new Get(new Key("c1", "aaa")).forNamespace("ns").forTable("tbl"); + + // Act + tableMetadataManager.getTableMetadata(get); + // Wait for cache to be expired + Uninterruptibles.sleepUninterruptibly(1200, TimeUnit.MILLISECONDS); + TableMetadata actualTableMetadata = tableMetadataManager.getTableMetadata(get); + + // Assert + verify(config).getTableMetadataCacheExpirationTimeSecs(); + verify(admin, times(2)).getTableMetadata(anyString(), anyString()); + assertThat(actualTableMetadata).isEqualTo(expectedTableMetadata); + } }