This is an automated email from the ASF dual-hosted git repository.
emaynard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new 510bd7264 Add a weigher to the EntityCache based on approximate entity
size (#490)
510bd7264 is described below
commit 510bd7264c668bc2c72c7d0be64e41dd373e3056
Author: Eric Maynard <[email protected]>
AuthorDate: Fri Apr 11 11:04:23 2025 -0700
Add a weigher to the EntityCache based on approximate entity size (#490)
* initial commit
* autolint
* resolve conflicts
* autolint
* pull main
* Add multiplier
* account for name, too
* adjust multiplier
* add config
* autolint
* remove old cast
* more tests, fixes per review
* add precise weight test
* autolint
---
.../core/config/BehaviorChangeConfiguration.java | 7 ++
.../polaris/core/config/FeatureConfiguration.java | 11 +++
.../core/persistence/cache/EntityCache.java | 22 +++--
.../core/persistence/cache/EntityWeigher.java | 69 +++++++++++++
.../persistence/{ => cache}/EntityCacheTest.java | 33 ++++++-
.../core/persistence/cache/EntityWeigherTest.java | 109 +++++++++++++++++++++
.../persistence/PolarisTestMetaStoreManager.java | 12 +--
.../org/apache/polaris/service/TestServices.java | 9 +-
8 files changed, 252 insertions(+), 20 deletions(-)
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 c3e9f7e5f..d3577a30f 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 @@ public class BehaviorChangeConfiguration<T> extends
PolarisConfiguration<T> {
+ " unlimited locations")
.defaultValue(-1)
.buildBehaviorChangeConfiguration();
+
+ public static final BehaviorChangeConfiguration<Boolean>
ENTITY_CACHE_SOFT_VALUES =
+ PolarisConfiguration.<Boolean>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 8e71d049c..d03a55b2f 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 @@ package org.apache.polaris.core.config;
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 @@ public class FeatureConfiguration<T> extends
PolarisConfiguration<T> {
.description("If true, the generic-tables endpoints are enabled")
.defaultValue(true)
.buildFeatureConfiguration();
+
+ public static final FeatureConfiguration<Long> ENTITY_CACHE_WEIGHER_TARGET =
+ PolarisConfiguration.<Long>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 2f2476a6b..752342ffe 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.List;
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 class EntityCache {
}
};
- // 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<Long, ResolvedPolarisEntity> 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 000000000..b3409d3b9
--- /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<Long, ResolvedPolarisEntity> {
+
+ /** 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<Long, ResolvedPolarisEntity> 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 6e2f458dc..c21bef002 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 @@ public class EntityCacheTest {
// 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 000000000..e989329c5
--- /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<String> 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 8bcbe431f..e17974e0c 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
@@ -916,7 +916,7 @@ public class PolarisTestMetaStoreManager {
}
/** Grant a privilege to a catalog role */
- void grantPrivilege(
+ public void grantPrivilege(
PolarisBaseEntity role,
List<PolarisEntityCore> catalogPath,
PolarisBaseEntity securable,
@@ -1303,7 +1303,7 @@ public class PolarisTestMetaStoreManager {
*
* @return the identity we found
*/
- PolarisBaseEntity ensureExistsByName(
+ public PolarisBaseEntity ensureExistsByName(
List<PolarisEntityCore> catalogPath,
PolarisEntityType entityType,
PolarisEntitySubType entitySubType,
@@ -1349,7 +1349,7 @@ public class PolarisTestMetaStoreManager {
*
* @return the identity we found
*/
- PolarisBaseEntity ensureExistsByName(
+ public PolarisBaseEntity ensureExistsByName(
List<PolarisEntityCore> catalogPath, PolarisEntityType entityType,
String name) {
return this.ensureExistsByName(
catalogPath, entityType, PolarisEntitySubType.NULL_SUBTYPE, name);
@@ -1364,7 +1364,7 @@ public class PolarisTestMetaStoreManager {
* @param internalProps updated internal properties
* @return updated entity
*/
- PolarisBaseEntity updateEntity(
+ public PolarisBaseEntity updateEntity(
List<PolarisEntityCore> catalogPath,
PolarisBaseEntity entity,
String props,
@@ -1858,7 +1858,7 @@ public class PolarisTestMetaStoreManager {
this.ensureGrantRecordExists(principalRole, principal,
PolarisPrivilege.PRINCIPAL_ROLE_USAGE);
}
- void testCreateTestCatalog() {
+ public void testCreateTestCatalog() {
// create test catalog
this.createTestCatalog("test");
@@ -2432,7 +2432,7 @@ public class PolarisTestMetaStoreManager {
* @param newCatPath new catalog path
* @param newName new name
*/
- void renameEntity(
+ public void renameEntity(
List<PolarisEntityCore> catPath,
PolarisBaseEntity entity,
List<PolarisEntityCore> 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 d55021792..bfe97d8ca 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 record TestServices(
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 record TestServices(
return new HashMap<>();
}
};
+ CallContext.setCurrentContext(callContext);
+ PolarisEntityManager entityManager =
+ realmEntityManagerFactory.getOrCreateEntityManager(realmContext);
+ PolarisMetaStoreManager metaStoreManager =
+ metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext);
FileIOFactory fileIOFactory =
fileIOFactorySupplier.apply(