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(

Reply via email to