diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java index c3e9f7e5fe..d3577a30f3 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java @@ -52,4 +52,11 @@ protected BehaviorChangeConfiguration( + " unlimited locations") .defaultValue(-1) .buildBehaviorChangeConfiguration(); + + public static final BehaviorChangeConfiguration ENTITY_CACHE_SOFT_VALUES = + PolarisConfiguration.builder() + .key("ENTITY_CACHE_SOFT_VALUES") + .description("Whether or not to use soft values in the entity cache") + .defaultValue(false) + .buildBehaviorChangeConfiguration(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index cc8bae454d..6be57f178b 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Optional; import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.core.persistence.cache.EntityWeigher; /** * Configurations for features within Polaris. These configurations are intended to be customized @@ -190,4 +191,14 @@ protected FeatureConfiguration( .description("If true, the generic-tables endpoints are enabled") .defaultValue(false) .buildFeatureConfiguration(); + + public static final FeatureConfiguration ENTITY_CACHE_WEIGHER_TARGET = + PolarisConfiguration.builder() + .key("ENTITY_CACHE_WEIGHER_TARGET") + .description( + "The maximum weight for the entity cache. This is a heuristic value without any particular" + + " unit of measurement. It roughly correlates with the total heap size of cached values. Fine-tuning" + + " requires experimentation in the specific deployment environment") + .defaultValue(100 * EntityWeigher.WEIGHT_PER_MB) + .buildFeatureConfiguration(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityCache.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityCache.java index 2f2476a6bc..752342ffe6 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityCache.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityCache.java @@ -28,6 +28,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.config.BehaviorChangeConfiguration; +import org.apache.polaris.core.config.FeatureConfiguration; +import org.apache.polaris.core.config.PolarisConfiguration; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisGrantRecord; @@ -72,14 +75,21 @@ public EntityCache(@Nonnull PolarisMetaStoreManager polarisMetaStoreManager) { } }; - // use a Caffeine cache to purge entries when those have not been used for a long time. - // Assuming 1KB per entry, 100K entries is about 100MB. - this.byId = + long weigherTarget = + PolarisConfiguration.loadConfig(FeatureConfiguration.ENTITY_CACHE_WEIGHER_TARGET); + Caffeine byIdBuilder = Caffeine.newBuilder() - .maximumSize(100_000) // Set maximum size to 100,000 elements + .maximumWeight(weigherTarget) + .weigher(EntityWeigher.asWeigher()) .expireAfterAccess(1, TimeUnit.HOURS) // Expire entries after 1 hour of no access - .removalListener(removalListener) // Set the removal listener - .build(); + .removalListener(removalListener); // Set the removal listener + + if (PolarisConfiguration.loadConfig(BehaviorChangeConfiguration.ENTITY_CACHE_SOFT_VALUES)) { + byIdBuilder.softValues(); + } + + // use a Caffeine cache to purge entries when those have not been used for a long time. + this.byId = byIdBuilder.build(); // remember the meta store manager this.polarisMetaStoreManager = polarisMetaStoreManager; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityWeigher.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityWeigher.java new file mode 100644 index 0000000000..b3409d3b90 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityWeigher.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.persistence.cache; + +import com.github.benmanes.caffeine.cache.Weigher; +import org.apache.polaris.core.persistence.ResolvedPolarisEntity; +import org.checkerframework.checker.index.qual.NonNegative; + +/** + * A {@link Weigher} implementation that weighs {@link ResolvedPolarisEntity} objects by the + * approximate size of the entity object. + */ +public class EntityWeigher implements Weigher { + + /** The amount of weight that is expected to roughly equate to 1MB of memory usage */ + public static final long WEIGHT_PER_MB = 1024 * 1024; + + /* Represents the approximate size of an entity beyond the properties */ + private static final int APPROXIMATE_ENTITY_OVERHEAD = 1000; + + /* Represents the amount of bytes that a character is expected to take up */ + private static final int APPROXIMATE_BYTES_PER_CHAR = 3; + + /** Singleton instance */ + private static final EntityWeigher instance = new EntityWeigher(); + + private EntityWeigher() {} + + /** Gets the singleton {@link EntityWeigher} */ + public static EntityWeigher getInstance() { + return instance; + } + + /** + * Computes the weight of a given entity. The unit here is not exactly bytes, but it's close. + * + * @param key The entity's key; not used + * @param value The entity to be cached + * @return The weight of the entity + */ + @Override + public @NonNegative int weigh(Long key, ResolvedPolarisEntity value) { + return APPROXIMATE_ENTITY_OVERHEAD + + (value.getEntity().getName().length() * APPROXIMATE_BYTES_PER_CHAR) + + (value.getEntity().getProperties().length() * APPROXIMATE_BYTES_PER_CHAR) + + (value.getEntity().getInternalProperties().length() * APPROXIMATE_BYTES_PER_CHAR); + } + + /** Factory method to provide a typed Weigher */ + public static Weigher asWeigher() { + return getInstance(); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/cache/EntityCacheTest.java similarity index 94% rename from polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java rename to polaris-core/src/test/java/org/apache/polaris/core/persistence/cache/EntityCacheTest.java index 6e2f458dcb..c21bef0024 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/cache/EntityCacheTest.java @@ -16,23 +16,26 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.cache; import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS; import java.util.List; import java.util.stream.Collectors; +import org.apache.iceberg.catalog.TableIdentifier; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrivilege; -import org.apache.polaris.core.persistence.cache.EntityCache; -import org.apache.polaris.core.persistence.cache.EntityCacheByNameKey; -import org.apache.polaris.core.persistence.cache.EntityCacheLookupResult; +import org.apache.polaris.core.entity.table.IcebergTableLikeEntity; +import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.persistence.PolarisTestMetaStoreManager; +import org.apache.polaris.core.persistence.ResolvedPolarisEntity; import org.apache.polaris.core.persistence.transactional.TransactionalMetaStoreManagerImpl; import org.apache.polaris.core.persistence.transactional.TransactionalPersistence; import org.apache.polaris.core.persistence.transactional.TreeMapMetaStore; @@ -478,4 +481,26 @@ void testRenameAndCacheDestinationBeforeLoadingSource() { // now the loading by the old name should return null Assertions.assertThat(cache.getOrLoadEntityByName(callCtx, T4_name)).isNull(); } + + /* Helper for `testEntityWeigher` */ + private int getEntityWeight(PolarisEntity entity) { + return EntityWeigher.getInstance() + .weigh(-1L, new ResolvedPolarisEntity(diagServices, entity, List.of(), 1)); + } + + @Test + void testEntityWeigher() { + var smallEntity = new IcebergTableLikeEntity.Builder(TableIdentifier.of("ns.t1"), "").build(); + var mediumEntity = + new IcebergTableLikeEntity.Builder(TableIdentifier.of("ns.t1"), "") + .setMetadataLocation("a".repeat(10000)) + .build(); + var largeEntity = + new IcebergTableLikeEntity.Builder(TableIdentifier.of("ns.t1"), "") + .setMetadataLocation("a".repeat(1000 * 1000)) + .build(); + + Assertions.assertThat(getEntityWeight(smallEntity)).isLessThan(getEntityWeight(mediumEntity)); + Assertions.assertThat(getEntityWeight(mediumEntity)).isLessThan(getEntityWeight(largeEntity)); + } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/cache/EntityWeigherTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/cache/EntityWeigherTest.java new file mode 100644 index 0000000000..e989329c5c --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/cache/EntityWeigherTest.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.persistence.cache; + +import java.util.List; +import java.util.Optional; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; +import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.entity.table.IcebergTableLikeEntity; +import org.apache.polaris.core.persistence.ResolvedPolarisEntity; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class EntityWeigherTest { + + private PolarisDiagnostics diagnostics; + + public EntityWeigherTest() { + diagnostics = new PolarisDefaultDiagServiceImpl(); + } + + private ResolvedPolarisEntity getEntity( + String name, + String metadataLocation, + String properties, + Optional internalProperties) { + var entity = + new IcebergTableLikeEntity.Builder(TableIdentifier.of(name), metadataLocation).build(); + entity.setProperties(properties); + internalProperties.ifPresent(p -> entity.setInternalProperties(p)); + return new ResolvedPolarisEntity(diagnostics, entity, List.of(), 1); + } + + @Test + public void testBasicWeight() { + int weight = EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty())); + Assertions.assertThat(weight).isGreaterThan(0); + } + + @Test + public void testNonZeroWeight() { + int weight = EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.of(""))); + Assertions.assertThat(weight).isGreaterThan(0); + } + + @Test + public void testWeightIncreasesWithNameLength() { + int smallWeight = + EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty())); + int largeWeight = + EntityWeigher.getInstance().weigh(1L, getEntity("looong name", "", "", Optional.empty())); + Assertions.assertThat(smallWeight).isLessThan(largeWeight); + } + + @Test + public void testWeightIncreasesWithMetadataLocationLength() { + int smallWeight = + EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty())); + int largeWeight = + EntityWeigher.getInstance() + .weigh(1L, getEntity("t", "looong location", "", Optional.empty())); + Assertions.assertThat(smallWeight).isLessThan(largeWeight); + } + + @Test + public void testWeightIncreasesWithPropertiesLength() { + int smallWeight = + EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty())); + int largeWeight = + EntityWeigher.getInstance() + .weigh(1L, getEntity("t", "", "looong properties", Optional.empty())); + Assertions.assertThat(smallWeight).isLessThan(largeWeight); + } + + @Test + public void testWeightIncreasesWithInternalPropertiesLength() { + int smallWeight = + EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.of(""))); + int largeWeight = + EntityWeigher.getInstance() + .weigh(1L, getEntity("t", "", "", Optional.of("looong iproperties"))); + Assertions.assertThat(smallWeight).isLessThan(largeWeight); + } + + @Test + public void testExactWeightCalculation() { + int preciseWeight = + EntityWeigher.getInstance() + .weigh(1L, getEntity("name", "location", "{a: b}", Optional.of("{c: d, e: f}"))); + Assertions.assertThat(preciseWeight).isEqualTo(1066); + } +} diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java index 3e7a0fb2ac..c665ef033b 100644 --- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java @@ -904,7 +904,7 @@ void dropEntity(List catalogPath, PolarisBaseEntity entityToD } /** Grant a privilege to a catalog role */ - void grantPrivilege( + public void grantPrivilege( PolarisBaseEntity role, List catalogPath, PolarisBaseEntity securable, @@ -1291,7 +1291,7 @@ PolarisBaseEntity createTestCatalog(String catalogName) { * * @return the identity we found */ - PolarisBaseEntity ensureExistsByName( + public PolarisBaseEntity ensureExistsByName( List catalogPath, PolarisEntityType entityType, PolarisEntitySubType entitySubType, @@ -1337,7 +1337,7 @@ PolarisBaseEntity ensureExistsByName( * * @return the identity we found */ - PolarisBaseEntity ensureExistsByName( + public PolarisBaseEntity ensureExistsByName( List catalogPath, PolarisEntityType entityType, String name) { return this.ensureExistsByName( catalogPath, entityType, PolarisEntitySubType.NULL_SUBTYPE, name); @@ -1352,7 +1352,7 @@ PolarisBaseEntity ensureExistsByName( * @param internalProps updated internal properties * @return updated entity */ - PolarisBaseEntity updateEntity( + public PolarisBaseEntity updateEntity( List catalogPath, PolarisBaseEntity entity, String props, @@ -1844,7 +1844,7 @@ void validateBootstrap() { this.ensureGrantRecordExists(principalRole, principal, PolarisPrivilege.PRINCIPAL_ROLE_USAGE); } - void testCreateTestCatalog() { + public void testCreateTestCatalog() { // create test catalog this.createTestCatalog("test"); @@ -2418,7 +2418,7 @@ public void testPrivileges() { * @param newCatPath new catalog path * @param newName new name */ - void renameEntity( + public void renameEntity( List catPath, PolarisBaseEntity entity, List newCatPath, diff --git a/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java index d550217926..bfe97d8ca5 100644 --- a/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -133,10 +133,6 @@ public TestServices build() { RealmEntityManagerFactory realmEntityManagerFactory = new RealmEntityManagerFactory(metaStoreManagerFactory) {}; - PolarisEntityManager entityManager = - realmEntityManagerFactory.getOrCreateEntityManager(realmContext); - PolarisMetaStoreManager metaStoreManager = - metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); TransactionalPersistence metaStoreSession = metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(); CallContext callContext = @@ -160,6 +156,11 @@ public Map contextVariables() { return new HashMap<>(); } }; + CallContext.setCurrentContext(callContext); + PolarisEntityManager entityManager = + realmEntityManagerFactory.getOrCreateEntityManager(realmContext); + PolarisMetaStoreManager metaStoreManager = + metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); FileIOFactory fileIOFactory = fileIOFactorySupplier.apply(