Skip to content

Commit 510bd72

Browse files
authored
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
1 parent 97a49e0 commit 510bd72

File tree

8 files changed

+252
-20
lines changed

8 files changed

+252
-20
lines changed

polaris-core/src/main/java/org/apache/polaris/core/config/BehaviorChangeConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,11 @@ protected BehaviorChangeConfiguration(
5252
+ " unlimited locations")
5353
.defaultValue(-1)
5454
.buildBehaviorChangeConfiguration();
55+
56+
public static final BehaviorChangeConfiguration<Boolean> ENTITY_CACHE_SOFT_VALUES =
57+
PolarisConfiguration.<Boolean>builder()
58+
.key("ENTITY_CACHE_SOFT_VALUES")
59+
.description("Whether or not to use soft values in the entity cache")
60+
.defaultValue(false)
61+
.buildBehaviorChangeConfiguration();
5562
}

polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Optional;
2323
import org.apache.polaris.core.admin.model.StorageConfigInfo;
24+
import org.apache.polaris.core.persistence.cache.EntityWeigher;
2425

2526
/**
2627
* Configurations for features within Polaris. These configurations are intended to be customized
@@ -190,4 +191,14 @@ protected FeatureConfiguration(
190191
.description("If true, the generic-tables endpoints are enabled")
191192
.defaultValue(true)
192193
.buildFeatureConfiguration();
194+
195+
public static final FeatureConfiguration<Long> ENTITY_CACHE_WEIGHER_TARGET =
196+
PolarisConfiguration.<Long>builder()
197+
.key("ENTITY_CACHE_WEIGHER_TARGET")
198+
.description(
199+
"The maximum weight for the entity cache. This is a heuristic value without any particular"
200+
+ " unit of measurement. It roughly correlates with the total heap size of cached values. Fine-tuning"
201+
+ " requires experimentation in the specific deployment environment")
202+
.defaultValue(100 * EntityWeigher.WEIGHT_PER_MB)
203+
.buildFeatureConfiguration();
193204
}

polaris-core/src/main/java/org/apache/polaris/core/persistence/cache/EntityCache.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import java.util.concurrent.ConcurrentHashMap;
2929
import java.util.concurrent.TimeUnit;
3030
import org.apache.polaris.core.PolarisCallContext;
31+
import org.apache.polaris.core.config.BehaviorChangeConfiguration;
32+
import org.apache.polaris.core.config.FeatureConfiguration;
33+
import org.apache.polaris.core.config.PolarisConfiguration;
3134
import org.apache.polaris.core.entity.PolarisBaseEntity;
3235
import org.apache.polaris.core.entity.PolarisEntityType;
3336
import org.apache.polaris.core.entity.PolarisGrantRecord;
@@ -72,14 +75,21 @@ public EntityCache(@Nonnull PolarisMetaStoreManager polarisMetaStoreManager) {
7275
}
7376
};
7477

75-
// use a Caffeine cache to purge entries when those have not been used for a long time.
76-
// Assuming 1KB per entry, 100K entries is about 100MB.
77-
this.byId =
78+
long weigherTarget =
79+
PolarisConfiguration.loadConfig(FeatureConfiguration.ENTITY_CACHE_WEIGHER_TARGET);
80+
Caffeine<Long, ResolvedPolarisEntity> byIdBuilder =
7881
Caffeine.newBuilder()
79-
.maximumSize(100_000) // Set maximum size to 100,000 elements
82+
.maximumWeight(weigherTarget)
83+
.weigher(EntityWeigher.asWeigher())
8084
.expireAfterAccess(1, TimeUnit.HOURS) // Expire entries after 1 hour of no access
81-
.removalListener(removalListener) // Set the removal listener
82-
.build();
85+
.removalListener(removalListener); // Set the removal listener
86+
87+
if (PolarisConfiguration.loadConfig(BehaviorChangeConfiguration.ENTITY_CACHE_SOFT_VALUES)) {
88+
byIdBuilder.softValues();
89+
}
90+
91+
// use a Caffeine cache to purge entries when those have not been used for a long time.
92+
this.byId = byIdBuilder.build();
8393

8494
// remember the meta store manager
8595
this.polarisMetaStoreManager = polarisMetaStoreManager;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.persistence.cache;
20+
21+
import com.github.benmanes.caffeine.cache.Weigher;
22+
import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
23+
import org.checkerframework.checker.index.qual.NonNegative;
24+
25+
/**
26+
* A {@link Weigher} implementation that weighs {@link ResolvedPolarisEntity} objects by the
27+
* approximate size of the entity object.
28+
*/
29+
public class EntityWeigher implements Weigher<Long, ResolvedPolarisEntity> {
30+
31+
/** The amount of weight that is expected to roughly equate to 1MB of memory usage */
32+
public static final long WEIGHT_PER_MB = 1024 * 1024;
33+
34+
/* Represents the approximate size of an entity beyond the properties */
35+
private static final int APPROXIMATE_ENTITY_OVERHEAD = 1000;
36+
37+
/* Represents the amount of bytes that a character is expected to take up */
38+
private static final int APPROXIMATE_BYTES_PER_CHAR = 3;
39+
40+
/** Singleton instance */
41+
private static final EntityWeigher instance = new EntityWeigher();
42+
43+
private EntityWeigher() {}
44+
45+
/** Gets the singleton {@link EntityWeigher} */
46+
public static EntityWeigher getInstance() {
47+
return instance;
48+
}
49+
50+
/**
51+
* Computes the weight of a given entity. The unit here is not exactly bytes, but it's close.
52+
*
53+
* @param key The entity's key; not used
54+
* @param value The entity to be cached
55+
* @return The weight of the entity
56+
*/
57+
@Override
58+
public @NonNegative int weigh(Long key, ResolvedPolarisEntity value) {
59+
return APPROXIMATE_ENTITY_OVERHEAD
60+
+ (value.getEntity().getName().length() * APPROXIMATE_BYTES_PER_CHAR)
61+
+ (value.getEntity().getProperties().length() * APPROXIMATE_BYTES_PER_CHAR)
62+
+ (value.getEntity().getInternalProperties().length() * APPROXIMATE_BYTES_PER_CHAR);
63+
}
64+
65+
/** Factory method to provide a typed Weigher */
66+
public static Weigher<Long, ResolvedPolarisEntity> asWeigher() {
67+
return getInstance();
68+
}
69+
}

polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java renamed to polaris-core/src/test/java/org/apache/polaris/core/persistence/cache/EntityCacheTest.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,26 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
package org.apache.polaris.core.persistence;
19+
package org.apache.polaris.core.persistence.cache;
2020

2121
import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS;
2222

2323
import java.util.List;
2424
import java.util.stream.Collectors;
25+
import org.apache.iceberg.catalog.TableIdentifier;
2526
import org.apache.polaris.core.PolarisCallContext;
2627
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
2728
import org.apache.polaris.core.PolarisDiagnostics;
2829
import org.apache.polaris.core.entity.PolarisBaseEntity;
30+
import org.apache.polaris.core.entity.PolarisEntity;
2931
import org.apache.polaris.core.entity.PolarisEntitySubType;
3032
import org.apache.polaris.core.entity.PolarisEntityType;
3133
import org.apache.polaris.core.entity.PolarisGrantRecord;
3234
import org.apache.polaris.core.entity.PolarisPrivilege;
33-
import org.apache.polaris.core.persistence.cache.EntityCache;
34-
import org.apache.polaris.core.persistence.cache.EntityCacheByNameKey;
35-
import org.apache.polaris.core.persistence.cache.EntityCacheLookupResult;
35+
import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
36+
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
37+
import org.apache.polaris.core.persistence.PolarisTestMetaStoreManager;
38+
import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
3639
import org.apache.polaris.core.persistence.transactional.TransactionalMetaStoreManagerImpl;
3740
import org.apache.polaris.core.persistence.transactional.TransactionalPersistence;
3841
import org.apache.polaris.core.persistence.transactional.TreeMapMetaStore;
@@ -478,4 +481,26 @@ void testRenameAndCacheDestinationBeforeLoadingSource() {
478481
// now the loading by the old name should return null
479482
Assertions.assertThat(cache.getOrLoadEntityByName(callCtx, T4_name)).isNull();
480483
}
484+
485+
/* Helper for `testEntityWeigher` */
486+
private int getEntityWeight(PolarisEntity entity) {
487+
return EntityWeigher.getInstance()
488+
.weigh(-1L, new ResolvedPolarisEntity(diagServices, entity, List.of(), 1));
489+
}
490+
491+
@Test
492+
void testEntityWeigher() {
493+
var smallEntity = new IcebergTableLikeEntity.Builder(TableIdentifier.of("ns.t1"), "").build();
494+
var mediumEntity =
495+
new IcebergTableLikeEntity.Builder(TableIdentifier.of("ns.t1"), "")
496+
.setMetadataLocation("a".repeat(10000))
497+
.build();
498+
var largeEntity =
499+
new IcebergTableLikeEntity.Builder(TableIdentifier.of("ns.t1"), "")
500+
.setMetadataLocation("a".repeat(1000 * 1000))
501+
.build();
502+
503+
Assertions.assertThat(getEntityWeight(smallEntity)).isLessThan(getEntityWeight(mediumEntity));
504+
Assertions.assertThat(getEntityWeight(mediumEntity)).isLessThan(getEntityWeight(largeEntity));
505+
}
481506
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.persistence.cache;
20+
21+
import java.util.List;
22+
import java.util.Optional;
23+
import org.apache.iceberg.catalog.TableIdentifier;
24+
import org.apache.polaris.core.PolarisDefaultDiagServiceImpl;
25+
import org.apache.polaris.core.PolarisDiagnostics;
26+
import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
27+
import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
28+
import org.assertj.core.api.Assertions;
29+
import org.junit.jupiter.api.Test;
30+
31+
public class EntityWeigherTest {
32+
33+
private PolarisDiagnostics diagnostics;
34+
35+
public EntityWeigherTest() {
36+
diagnostics = new PolarisDefaultDiagServiceImpl();
37+
}
38+
39+
private ResolvedPolarisEntity getEntity(
40+
String name,
41+
String metadataLocation,
42+
String properties,
43+
Optional<String> internalProperties) {
44+
var entity =
45+
new IcebergTableLikeEntity.Builder(TableIdentifier.of(name), metadataLocation).build();
46+
entity.setProperties(properties);
47+
internalProperties.ifPresent(p -> entity.setInternalProperties(p));
48+
return new ResolvedPolarisEntity(diagnostics, entity, List.of(), 1);
49+
}
50+
51+
@Test
52+
public void testBasicWeight() {
53+
int weight = EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty()));
54+
Assertions.assertThat(weight).isGreaterThan(0);
55+
}
56+
57+
@Test
58+
public void testNonZeroWeight() {
59+
int weight = EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.of("")));
60+
Assertions.assertThat(weight).isGreaterThan(0);
61+
}
62+
63+
@Test
64+
public void testWeightIncreasesWithNameLength() {
65+
int smallWeight =
66+
EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty()));
67+
int largeWeight =
68+
EntityWeigher.getInstance().weigh(1L, getEntity("looong name", "", "", Optional.empty()));
69+
Assertions.assertThat(smallWeight).isLessThan(largeWeight);
70+
}
71+
72+
@Test
73+
public void testWeightIncreasesWithMetadataLocationLength() {
74+
int smallWeight =
75+
EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty()));
76+
int largeWeight =
77+
EntityWeigher.getInstance()
78+
.weigh(1L, getEntity("t", "looong location", "", Optional.empty()));
79+
Assertions.assertThat(smallWeight).isLessThan(largeWeight);
80+
}
81+
82+
@Test
83+
public void testWeightIncreasesWithPropertiesLength() {
84+
int smallWeight =
85+
EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.empty()));
86+
int largeWeight =
87+
EntityWeigher.getInstance()
88+
.weigh(1L, getEntity("t", "", "looong properties", Optional.empty()));
89+
Assertions.assertThat(smallWeight).isLessThan(largeWeight);
90+
}
91+
92+
@Test
93+
public void testWeightIncreasesWithInternalPropertiesLength() {
94+
int smallWeight =
95+
EntityWeigher.getInstance().weigh(1L, getEntity("t", "", "", Optional.of("")));
96+
int largeWeight =
97+
EntityWeigher.getInstance()
98+
.weigh(1L, getEntity("t", "", "", Optional.of("looong iproperties")));
99+
Assertions.assertThat(smallWeight).isLessThan(largeWeight);
100+
}
101+
102+
@Test
103+
public void testExactWeightCalculation() {
104+
int preciseWeight =
105+
EntityWeigher.getInstance()
106+
.weigh(1L, getEntity("name", "location", "{a: b}", Optional.of("{c: d, e: f}")));
107+
Assertions.assertThat(preciseWeight).isEqualTo(1066);
108+
}
109+
}

polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,7 @@ void dropEntity(List<PolarisEntityCore> catalogPath, PolarisBaseEntity entityToD
916916
}
917917

918918
/** Grant a privilege to a catalog role */
919-
void grantPrivilege(
919+
public void grantPrivilege(
920920
PolarisBaseEntity role,
921921
List<PolarisEntityCore> catalogPath,
922922
PolarisBaseEntity securable,
@@ -1303,7 +1303,7 @@ PolarisBaseEntity createTestCatalog(String catalogName) {
13031303
*
13041304
* @return the identity we found
13051305
*/
1306-
PolarisBaseEntity ensureExistsByName(
1306+
public PolarisBaseEntity ensureExistsByName(
13071307
List<PolarisEntityCore> catalogPath,
13081308
PolarisEntityType entityType,
13091309
PolarisEntitySubType entitySubType,
@@ -1349,7 +1349,7 @@ PolarisBaseEntity ensureExistsByName(
13491349
*
13501350
* @return the identity we found
13511351
*/
1352-
PolarisBaseEntity ensureExistsByName(
1352+
public PolarisBaseEntity ensureExistsByName(
13531353
List<PolarisEntityCore> catalogPath, PolarisEntityType entityType, String name) {
13541354
return this.ensureExistsByName(
13551355
catalogPath, entityType, PolarisEntitySubType.NULL_SUBTYPE, name);
@@ -1364,7 +1364,7 @@ PolarisBaseEntity ensureExistsByName(
13641364
* @param internalProps updated internal properties
13651365
* @return updated entity
13661366
*/
1367-
PolarisBaseEntity updateEntity(
1367+
public PolarisBaseEntity updateEntity(
13681368
List<PolarisEntityCore> catalogPath,
13691369
PolarisBaseEntity entity,
13701370
String props,
@@ -1858,7 +1858,7 @@ void validateBootstrap() {
18581858
this.ensureGrantRecordExists(principalRole, principal, PolarisPrivilege.PRINCIPAL_ROLE_USAGE);
18591859
}
18601860

1861-
void testCreateTestCatalog() {
1861+
public void testCreateTestCatalog() {
18621862
// create test catalog
18631863
this.createTestCatalog("test");
18641864

@@ -2432,7 +2432,7 @@ public void testPrivileges() {
24322432
* @param newCatPath new catalog path
24332433
* @param newName new name
24342434
*/
2435-
void renameEntity(
2435+
public void renameEntity(
24362436
List<PolarisEntityCore> catPath,
24372437
PolarisBaseEntity entity,
24382438
List<PolarisEntityCore> newCatPath,

service/common/src/testFixtures/java/org/apache/polaris/service/TestServices.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,6 @@ public TestServices build() {
133133
RealmEntityManagerFactory realmEntityManagerFactory =
134134
new RealmEntityManagerFactory(metaStoreManagerFactory) {};
135135

136-
PolarisEntityManager entityManager =
137-
realmEntityManagerFactory.getOrCreateEntityManager(realmContext);
138-
PolarisMetaStoreManager metaStoreManager =
139-
metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext);
140136
TransactionalPersistence metaStoreSession =
141137
metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get();
142138
CallContext callContext =
@@ -160,6 +156,11 @@ public Map<String, Object> contextVariables() {
160156
return new HashMap<>();
161157
}
162158
};
159+
CallContext.setCurrentContext(callContext);
160+
PolarisEntityManager entityManager =
161+
realmEntityManagerFactory.getOrCreateEntityManager(realmContext);
162+
PolarisMetaStoreManager metaStoreManager =
163+
metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext);
163164

164165
FileIOFactory fileIOFactory =
165166
fileIOFactorySupplier.apply(

0 commit comments

Comments
 (0)