diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java index c3dd8947ce87..c4269b169bd3 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/BlockCacheFactory.java @@ -166,7 +166,7 @@ public static CacheEngine createFirstLevelCacheEngine(final Configuration c) { return CacheEngines .fromBlockCache(new TinyLfuBlockCache(cacheSize, blockSize, ForkJoinPool.commonPool(), c)); } else if (policy.equalsIgnoreCase("AdaptiveLRU")) { - return CacheEngines.fromBlockCache(new LruAdaptiveBlockCache(cacheSize, blockSize, true, c)); + return new LruCacheEngine(cacheSize, blockSize, true, c); } else { throw new IllegalArgumentException("Unknown policy: " + policy); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/AdaptiveLruCachePlacementAdmissionPolicy.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/AdaptiveLruCachePlacementAdmissionPolicy.java new file mode 100644 index 000000000000..cf446a6608a4 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/AdaptiveLruCachePlacementAdmissionPolicy.java @@ -0,0 +1,308 @@ +/* + * 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.hadoop.hbase.io.hfile.cache; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.io.hfile.BlockCacheKey; +import org.apache.hadoop.hbase.io.hfile.Cacheable; +import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Placement and admission policy implementing AdaptiveLRU behavior using a native + * {@link LruCacheEngine}. + *
+ * The policy preserves the adaptive admission behavior of the legacy {@code LruAdaptiveBlockCache}. + * Under sustained LRU eviction pressure, it reduces the percentage of data blocks admitted to the + * first-level cache. When eviction pressure decreases, data-block admission is gradually restored. + *
+ *+ * Non-data blocks are not filtered by the adaptive algorithm. + *
+ */ +@InterfaceAudience.Private +public final class AdaptiveLruCachePlacementAdmissionPolicy + implements CachePlacementAdmissionPolicy { + + private static final Logger LOG = + LoggerFactory.getLogger(AdaptiveLruCachePlacementAdmissionPolicy.class); + + private static final String LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT = + "hbase.lru.cache.heavy.eviction.count.limit"; + + private static final int DEFAULT_LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT = Integer.MAX_VALUE; + + private static final String LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT = + "hbase.lru.cache.heavy.eviction.mb.size.limit"; + + private static final long DEFAULT_LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT = 500L; + + private static final String LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT = + "hbase.lru.cache.heavy.eviction.overhead.coefficient"; + + private static final float DEFAULT_LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT = 0.01f; + + private static final long EVICTION_EVALUATION_PERIOD_MS = 10_000L; + + private static final int MIN_CACHE_DATA_BLOCK_PERCENT = 1; + + private static final int MAX_CACHE_DATA_BLOCK_PERCENT = 100; + + private static final int MAX_ADMISSION_REDUCTION_PER_PERIOD = 15; + + private static final CachePlacementAdmissionPolicy DELEGATE = + DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE; + + private final int heavyEvictionCountLimit; + + private final long heavyEvictionMbSizeLimit; + + private final float heavyEvictionOverheadCoefficient; + + private volatile int cacheDataBlockPercent = MAX_CACHE_DATA_BLOCK_PERCENT; + + private long freedSumMb; + + private int heavyEvictionCount; + + private long evaluationStartTime; + + /** + * Creates an AdaptiveLRU placement/admission policy from configuration. + * @param conf cache configuration + * @throws NullPointerException if {@code conf} is {@code null} + */ + public AdaptiveLruCachePlacementAdmissionPolicy(Configuration conf) { + Objects.requireNonNull(conf, "conf must not be null"); + + this.heavyEvictionCountLimit = Math.max(conf.getInt(LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT, + DEFAULT_LRU_CACHE_HEAVY_EVICTION_COUNT_LIMIT), 0); + + this.heavyEvictionMbSizeLimit = Math.max(conf.getLong(LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT, + DEFAULT_LRU_CACHE_HEAVY_EVICTION_MB_SIZE_LIMIT), 1L); + + float configuredCoefficient = conf.getFloat(LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT, + DEFAULT_LRU_CACHE_HEAVY_EVICTION_OVERHEAD_COEFFICIENT); + configuredCoefficient = Math.min(configuredCoefficient, 1.0f); + configuredCoefficient = Math.max(configuredCoefficient, 0.001f); + this.heavyEvictionOverheadCoefficient = configuredCoefficient; + + this.evaluationStartTime = EnvironmentEdgeManager.currentTime(); + } + + /** + * Decides whether a block should be admitted into the cache. + *+ * In a single-tier topology, adaptive rejection means the data block is not cached. In a tiered + * topology, admission remains enabled because adaptive filtering applies specifically to the L1 + * placement decision rather than to the entire cache hierarchy. + *
+ * @param cacheKey block cache key + * @param block block to cache + * @param context cache write context + * @param priority admission priority + * @param topologyView read-only topology view + * @return admission decision + */ + @Override + public AdmissionDecision shouldAdmit(BlockCacheKey cacheKey, Cacheable block, + CacheWriteContext context, AdmissionPriority priority, CacheTopologyView topologyView) { + Objects.requireNonNull(cacheKey, "cacheKey must not be null"); + Objects.requireNonNull(block, "block must not be null"); + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(priority, "priority must not be null"); + Objects.requireNonNull(topologyView, "topologyView must not be null"); + + AdmissionDecision decision = + DELEGATE.shouldAdmit(cacheKey, block, context, priority, topologyView); + if (!decision.isAdmitted()) { + return decision; + } + + if ( + topologyView.getType() == CacheTopologyType.SINGLE_TIER + && !shouldCacheInFirstLevel(cacheKey, block) + ) { + return AdmissionDecision.reject("Rejected by AdaptiveLRU data-block admission"); + } + + return decision; + } + + /** + * Selects the target tier or tiers for an admitted block. + *+ * The default HBase placement decision is preserved unless AdaptiveLRU rejects a data block from + * L1. In a tiered topology this removes L1 from the target set while retaining other selected + * tiers such as L2. + *
+ * @param cacheKey block cache key + * @param block block to cache + * @param context cache write context + * @param topologyView read-only topology view + * @return cache tier decision + */ + @Override + public TierDecision selectTier(BlockCacheKey cacheKey, Cacheable block, CacheWriteContext context, + CacheTopologyView topologyView) { + Objects.requireNonNull(cacheKey, "cacheKey must not be null"); + Objects.requireNonNull(block, "block must not be null"); + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(topologyView, "topologyView must not be null"); + + TierDecision decision = DELEGATE.selectTier(cacheKey, block, context, topologyView); + + if (!decision.getTiers().contains(CacheTier.L1) || shouldCacheInFirstLevel(cacheKey, block)) { + return decision; + } + + List+ * Pressure from L2 is ignored because AdaptiveLRU controls admission into the first-level cache. + * The algorithm intentionally preserves the behavior of the legacy {@code LruAdaptiveBlockCache}, + * including its approximately ten-second evaluation window and megabyte-based pressure + * accounting. + *
+ * @param sourceTier tier whose engine experienced cache pressure + * @param bytesToFree amount of cache pressure observed, in bytes + * @param topologyView read-only topology view + */ + @Override + public synchronized void onEvictionPressure(CacheTier sourceTier, long bytesToFree, + CacheTopologyView topologyView) { + Objects.requireNonNull(sourceTier, "sourceTier must not be null"); + Objects.requireNonNull(topologyView, "topologyView must not be null"); + + if (sourceTier != CacheTier.SINGLE && sourceTier != CacheTier.L1) { + return; + } + + freedSumMb += Math.max(bytesToFree, 0L) / 1024L / 1024L; + + long stopTime = EnvironmentEdgeManager.currentTime(); + if (stopTime - evaluationStartTime <= EVICTION_EVALUATION_PERIOD_MS - 1L) { + return; + } + + int freedDataOverheadPercent = (int) (freedSumMb * 100L / heavyEvictionMbSizeLimit) - 100; + + if (freedSumMb > heavyEvictionMbSizeLimit) { + heavyEvictionCount++; + + if (heavyEvictionCount > heavyEvictionCountLimit) { + int change = (int) (freedDataOverheadPercent * heavyEvictionOverheadCoefficient); + change = Math.min(MAX_ADMISSION_REDUCTION_PER_PERIOD, change); + change = Math.max(0, change); + + cacheDataBlockPercent -= change; + cacheDataBlockPercent = Math.max(MIN_CACHE_DATA_BLOCK_PERCENT, cacheDataBlockPercent); + } + } else if (freedSumMb >= heavyEvictionMbSizeLimit * 0.1) { + int change = (int) (-freedDataOverheadPercent * 0.1 + 1); + cacheDataBlockPercent += change; + cacheDataBlockPercent = Math.min(MAX_CACHE_DATA_BLOCK_PERCENT, cacheDataBlockPercent); + } else { + heavyEvictionCount = 0; + cacheDataBlockPercent = MAX_CACHE_DATA_BLOCK_PERCENT; + } + + LOG.info( + "BlockCache evicted (MB): {}, overhead (%): {}, heavy eviction counter: {}, " + + "current caching DataBlock (%): {}", + freedSumMb, freedDataOverheadPercent, heavyEvictionCount, cacheDataBlockPercent); + + freedSumMb = 0L; + evaluationStartTime = stopTime; + } + + /** + * Returns whether the block should be cached in the first-level AdaptiveLRU cache. + *+ * Non-data blocks are always accepted. Data blocks use the same deterministic offset-based + * filtering used by the legacy AdaptiveLRU implementation. + *
+ * @param cacheKey block cache key + * @param block block being evaluated + * @return {@code true} if the block should be cached in the first-level cache + */ + private boolean shouldCacheInFirstLevel(BlockCacheKey cacheKey, Cacheable block) { + if (!block.getBlockType().isData() || cacheDataBlockPercent == MAX_CACHE_DATA_BLOCK_PERCENT) { + return true; + } + + return cacheKey.getOffset() % 100 < cacheDataBlockPercent; + } + + /** + * Returns the current percentage of data blocks admitted to the first-level cache. + *+ * This method is intended for tests and diagnostics. + *
+ * @return current data-block admission percentage + */ + int getCacheDataBlockPercent() { + return cacheDataBlockPercent; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java index 050ab87f3dde..3e26024309f6 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServices.java @@ -108,7 +108,7 @@ public static CacheAccessService fromConfiguration(Configuration conf, return disabled(); } - CachePlacementAdmissionPolicy policy = DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE; + CachePlacementAdmissionPolicy policy = createPlacementAdmissionPolicy(conf); boolean useExternal = conf.getBoolean(BlockCacheFactory.EXTERNAL_BLOCKCACHE_KEY, BlockCacheFactory.EXTERNAL_BLOCKCACHE_DEFAULT); @@ -183,4 +183,22 @@ public static Optional- * The listener is intended for topology-level handling of capacity-driven evictions. In particular, - * an exclusive tiered topology may use an eviction from L1 as a request to demote the block to L2. + * Per-block eviction events may be used by a cache topology for operations such as L1-to-L2 + * demotion. Eviction-pressure events may be used by placement/admission policies to adjust future + * cache decisions. *
*- * The supplied block remains valid for the duration of the callback. An implementation that needs - * to retain the block after the callback returns must establish its own reference. + * A block supplied to {@link #onEviction(CacheEngine, BlockCacheKey, Cacheable)} remains valid for + * the duration of the callback. An implementation that needs to retain the block after the callback + * returns must establish its own reference. *
*/ @InterfaceAudience.Private @@ -42,4 +44,19 @@ public interface CacheEvictionListener { * @param block evicted block */ void onEviction(CacheEngine sourceEngine, BlockCacheKey cacheKey, Cacheable block); + + /** + * Handles cache pressure observed during a capacity-driven eviction pass. + *+ * {@code bytesToFree} is the amount of space the engine determined should be freed to reach its + * target size. It does not necessarily equal the exact number of bytes actually evicted. + *
+ *+ * The default implementation does nothing. + *
+ * @param sourceEngine engine that performed the eviction pass + * @param bytesToFree amount of cache pressure observed, in bytes + */ + default void onEvictionPressure(CacheEngine sourceEngine, long bytesToFree) { + } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java index 7ce89fdceeba..ca8f6e69879f 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CachePlacementAdmissionPolicy.java @@ -85,4 +85,18 @@ RepresentationDecision selectRepresentation(BlockCacheKey cacheKey, Cacheable bl */ PromotionDecision shouldPromote(BlockCacheKey cacheKey, Cacheable block, CacheTier sourceTier, CacheRequestContext context, CacheTopologyView topologyView); + + /** + * Notifies this policy about cache pressure observed during an engine eviction pass. + *+ * Stateful admission and placement policies may use this feedback to adjust future admission or + * placement decisions. The default implementation does nothing. + *
+ * @param sourceTier tier whose engine experienced cache pressure + * @param bytesToFree amount of cache pressure observed, in bytes + * @param topologyView read-only topology view + */ + default void onEvictionPressure(CacheTier sourceTier, long bytesToFree, + CacheTopologyView topologyView) { + } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java index 53ad44e45d57..4646f60a588f 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java @@ -804,11 +804,14 @@ void evict() { return; } + long bytesToFree = 0L; + boolean evictionPassCompleted = false; + try { evictionInProgress = true; long currentSize = size.get(); - long bytesToFree = currentSize - minSize(); + bytesToFree = currentSize - minSize(); if (LOG.isTraceEnabled()) { LOG.trace("LRU cache eviction started; Attempting to free " @@ -816,6 +819,7 @@ void evict() { } if (bytesToFree <= 0) { + evictionPassCompleted = true; return; } @@ -908,10 +912,27 @@ void evict() { + StringUtils.byteDesc(single) + ", multi=" + StringUtils.byteDesc(multi) + ", memory=" + StringUtils.byteDesc(memory)); } + + evictionPassCompleted = true; } finally { stats.evict(); evictionInProgress = false; evictionLock.unlock(); + + if (evictionPassCompleted) { + notifyEvictionPressure(Math.max(bytesToFree, 0L)); + } + } + } + + /** + * Notifies the configured eviction listener about pressure observed during an eviction pass. + * @param bytesToFree amount of cache pressure observed, in bytes + */ + private void notifyEvictionPressure(long bytesToFree) { + CacheEvictionListener listener = evictionListener; + if (listener != null) { + listener.onEvictionPressure(this, bytesToFree); } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java index 5c5034172cf0..fde0a33812b8 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessService.java @@ -79,11 +79,48 @@ public TopologyBackedCacheAccessService(CacheTopology topology, Objects.requireNonNull(topology.getView(), "topology view must not be null"); for (CacheTier tier : topology.getTiers()) { topology.getEngine(tier) - .ifPresent(engine -> engine.setEvictionListener(this::handleEngineEviction)); + .ifPresent(engine -> engine.setEvictionListener(createEvictionListener(tier))); } this.persistenceCoordinator = new CachePersistenceCoordinator(topology, policy); } + /** + * Creates an eviction listener associated with the specified cache tier. + *+ * Per-block eviction events are forwarded to the topology for topology-specific handling. + * Eviction-pressure events are forwarded to the placement/admission policy. + *
+ * @param tier tier associated with the cache engine + * @return eviction listener for the tier + */ + private CacheEvictionListener createEvictionListener(CacheTier tier) { + Objects.requireNonNull(tier, "tier must not be null"); + + return new CacheEvictionListener() { + + /** + * Handles a capacity-driven block eviction. + * @param sourceEngine engine that evicted the block + * @param cacheKey key identifying the evicted block + * @param block evicted block + */ + @Override + public void onEviction(CacheEngine sourceEngine, BlockCacheKey cacheKey, Cacheable block) { + handleEngineEviction(sourceEngine, cacheKey, block); + } + + /** + * Handles cache pressure reported by an eviction pass. + * @param sourceEngine engine that performed the eviction pass + * @param bytesToFree amount of cache pressure observed, in bytes + */ + @Override + public void onEvictionPressure(CacheEngine sourceEngine, long bytesToFree) { + policy.onEvictionPressure(tier, bytesToFree, topologyView); + } + }; + } + /** * Returns the topology used by this service. *
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java
index 52d5bf704c3a..e750a8965cbd 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheConfig.java
@@ -42,6 +42,7 @@
import org.apache.hadoop.hbase.io.ByteBuffAllocator;
import org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory;
import org.apache.hadoop.hbase.io.hfile.bucket.BucketCache;
+import org.apache.hadoop.hbase.io.hfile.cache.AdaptiveLruCachePlacementAdmissionPolicy;
import org.apache.hadoop.hbase.io.hfile.cache.BlockCacheBackedCacheEngine;
import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessService;
import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessServiceTestFactory;
@@ -499,4 +500,38 @@ public void testDeregistersCacheAccessServiceAsConfigurationChild() {
verify(manager).deregisterObserver(cacheAccessService);
}
+
+ /**
+ * Verifies that AdaptiveLRU uses the native LRU engine together with the adaptive placement and
+ * admission policy in the CacheAccessService construction path.
+ */
+ @Test
+ public void testAdaptiveLruCacheConfigUsesNativeEngineAndAdaptivePolicy() {
+ this.conf.set(BlockCacheFactory.BLOCKCACHE_POLICY_KEY, "AdaptiveLRU");
+
+ CacheAccessService service = CacheAccessServiceTestFactory.fromConfiguration(this.conf);
+
+ assertInstanceOf(TopologyBackedCacheAccessService.class, service);
+
+ CacheEngine engine = CacheAccessServiceTestFactory.getCacheEngine(service, CacheTier.SINGLE);
+ assertInstanceOf(LruCacheEngine.class, engine);
+
+ TopologyBackedCacheAccessService topologyBackedService =
+ (TopologyBackedCacheAccessService) service;
+ assertInstanceOf(AdaptiveLruCachePlacementAdmissionPolicy.class,
+ topologyBackedService.getPolicy());
+ }
+
+ /**
+ * Verifies that the legacy BlockCache construction path continues to create
+ * LruAdaptiveBlockCache.
+ */
+ @Test
+ public void testLegacyAdaptiveLruBlockCacheConfigurationIsPreserved() {
+ this.conf.set(BlockCacheFactory.BLOCKCACHE_POLICY_KEY, "AdaptiveLRU");
+
+ BlockCache blockCache = BlockCacheFactory.createBlockCache(this.conf);
+
+ assertInstanceOf(LruAdaptiveBlockCache.class, blockCache);
+ }
}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestAdaptiveLruCachePlacementAdmissionPolicy.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestAdaptiveLruCachePlacementAdmissionPolicy.java
new file mode 100644
index 000000000000..de2920188b70
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestAdaptiveLruCachePlacementAdmissionPolicy.java
@@ -0,0 +1,335 @@
+/*
+ * 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.hadoop.hbase.io.hfile.cache;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
+import org.apache.hadoop.hbase.io.hfile.BlockType;
+import org.apache.hadoop.hbase.io.hfile.Cacheable;
+import org.apache.hadoop.hbase.testclassification.IOTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
+import org.apache.hadoop.hbase.util.ManualEnvironmentEdge;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestAdaptiveLruCachePlacementAdmissionPolicy {
+
+ private static final String HEAVY_EVICTION_COUNT_LIMIT =
+ "hbase.lru.cache.heavy.eviction.count.limit";
+
+ private static final String HEAVY_EVICTION_MB_SIZE_LIMIT =
+ "hbase.lru.cache.heavy.eviction.mb.size.limit";
+
+ private static final String HEAVY_EVICTION_OVERHEAD_COEFFICIENT =
+ "hbase.lru.cache.heavy.eviction.overhead.coefficient";
+
+ private static final long MB = 1024L * 1024L;
+
+ private static final long EVALUATION_PERIOD_MS = 10_000L;
+
+ private ManualEnvironmentEdge environmentEdge;
+
+ private long currentTime;
+
+ /**
+ * Installs a manually controlled clock for AdaptiveLRU evaluation windows.
+ */
+ @BeforeEach
+ void setUp() {
+ currentTime = 1_000L;
+ environmentEdge = new ManualEnvironmentEdge();
+ environmentEdge.setValue(currentTime);
+ EnvironmentEdgeManager.injectEdge(environmentEdge);
+ }
+
+ /**
+ * Restores the normal environment clock after each test.
+ */
+ @AfterEach
+ void tearDown() {
+ EnvironmentEdgeManager.reset();
+ }
+
+ /**
+ * Verifies that AdaptiveLRU initially admits all data blocks.
+ */
+ @Test
+ void testInitialDataBlockAdmissionIsOneHundredPercent() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+
+ assertEquals(100, policy.getCacheDataBlockPercent());
+ }
+
+ /**
+ * Verifies that sustained eviction pressure reduces data-block admission.
+ */
+ @Test
+ void testHeavyEvictionPressureReducesDataBlockAdmission() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+
+ applyPressure(policy, CacheTier.SINGLE, singleTierView(), 3_200L);
+
+ assertEquals(85, policy.getCacheDataBlockPercent());
+ }
+
+ /**
+ * Verifies that the configured heavy-eviction count delays adaptive admission changes.
+ */
+ @Test
+ void testHeavyEvictionCountLimitDelaysAdaptation() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(1, 200L, 0.01f);
+ CacheTopologyView topologyView = singleTierView();
+
+ applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L);
+ assertEquals(100, policy.getCacheDataBlockPercent());
+
+ applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L);
+ assertEquals(85, policy.getCacheDataBlockPercent());
+ }
+
+ /**
+ * Verifies that admission increases again when eviction pressure decreases.
+ */
+ @Test
+ void testAdmissionRecoversWhenEvictionPressureDrops() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+ CacheTopologyView topologyView = singleTierView();
+
+ applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L);
+ assertEquals(85, policy.getCacheDataBlockPercent());
+
+ applyPressure(policy, CacheTier.SINGLE, topologyView, 20L);
+ assertEquals(95, policy.getCacheDataBlockPercent());
+
+ applyPressure(policy, CacheTier.SINGLE, topologyView, 0L);
+ assertEquals(100, policy.getCacheDataBlockPercent());
+ }
+
+ /**
+ * Verifies that pressure from L2 does not affect AdaptiveLRU admission.
+ */
+ @Test
+ void testSecondLevelEvictionPressureIsIgnored() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+
+ applyPressure(policy, CacheTier.L2, tieredExclusiveView(), 3_200L);
+
+ assertEquals(100, policy.getCacheDataBlockPercent());
+ }
+
+ /**
+ * Verifies the deterministic offset-based data-block admission filter for a single-tier cache.
+ */
+ @Test
+ void testSingleTierDataBlockAdmissionUsesBlockOffset() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+ CacheTopologyView topologyView = singleTierView();
+ Cacheable dataBlock = block(BlockType.DATA);
+ CacheWriteContext context = mock(CacheWriteContext.class);
+
+ applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L);
+ assertEquals(85, policy.getCacheDataBlockPercent());
+
+ AdmissionDecision admitted = policy.shouldAdmit(new BlockCacheKey("file", 84L), dataBlock,
+ context, AdmissionPriority.NORMAL, topologyView);
+
+ AdmissionDecision rejected = policy.shouldAdmit(new BlockCacheKey("file", 85L), dataBlock,
+ context, AdmissionPriority.NORMAL, topologyView);
+
+ assertTrue(admitted.isAdmitted());
+ assertFalse(rejected.isAdmitted());
+ }
+
+ /**
+ * Verifies that AdaptiveLRU does not filter metadata blocks.
+ */
+ @Test
+ void testMetadataBlocksAreNotAdaptivelyRejected() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+ CacheTopologyView topologyView = singleTierView();
+ Cacheable indexBlock = block(BlockType.ROOT_INDEX);
+ CacheWriteContext context = mock(CacheWriteContext.class);
+
+ applyPressure(policy, CacheTier.SINGLE, topologyView, 3_200L);
+
+ AdmissionDecision decision = policy.shouldAdmit(new BlockCacheKey("file", 99L), indexBlock,
+ context, AdmissionPriority.NORMAL, topologyView);
+
+ assertTrue(decision.isAdmitted());
+ }
+
+ /**
+ * Verifies that inclusive topology removes only L1 when AdaptiveLRU rejects a data block there.
+ */
+ @Test
+ void testInclusiveTopologyRetainsL2WhenDataBlockIsRejectedFromL1() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+ CacheTopologyView topologyView = tieredInclusiveView();
+ Cacheable dataBlock = block(BlockType.DATA);
+ CacheWriteContext context = mock(CacheWriteContext.class);
+
+ applyPressure(policy, CacheTier.L1, topologyView, 3_200L);
+ assertEquals(85, policy.getCacheDataBlockPercent());
+
+ BlockCacheKey cacheKey = new BlockCacheKey("file", 99L);
+
+ AdmissionDecision admission =
+ policy.shouldAdmit(cacheKey, dataBlock, context, AdmissionPriority.NORMAL, topologyView);
+ TierDecision placement = policy.selectTier(cacheKey, dataBlock, context, topologyView);
+
+ assertTrue(admission.isAdmitted());
+ assertEquals(List.of(CacheTier.L2), placement.getTiers());
+ }
+
+ /**
+ * Verifies that exclusive topology continues to place data blocks in L2 even when AdaptiveLRU
+ * would reject them from L1.
+ */
+ @Test
+ void testExclusiveTopologyKeepsDefaultDataPlacementInL2() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+ CacheTopologyView topologyView = tieredExclusiveView();
+ Cacheable dataBlock = block(BlockType.DATA);
+ CacheWriteContext context = mock(CacheWriteContext.class);
+
+ applyPressure(policy, CacheTier.L1, topologyView, 3_200L);
+ assertEquals(85, policy.getCacheDataBlockPercent());
+
+ BlockCacheKey cacheKey = new BlockCacheKey("file", 99L);
+
+ AdmissionDecision admission =
+ policy.shouldAdmit(cacheKey, dataBlock, context, AdmissionPriority.NORMAL, topologyView);
+ TierDecision placement = policy.selectTier(cacheKey, dataBlock, context, topologyView);
+
+ assertTrue(admission.isAdmitted());
+ assertEquals(List.of(CacheTier.L2), placement.getTiers());
+ }
+
+ /**
+ * Verifies that AdaptiveLRU returns no placement when the default placement contains only L1 and
+ * the data block is rejected from the first-level cache.
+ */
+ @Test
+ void testAdaptiveRejectionReturnsNoTierWhenOnlyL1IsAvailable() {
+ AdaptiveLruCachePlacementAdmissionPolicy policy = createPolicy(0, 200L, 0.01f);
+ CacheTopologyView topologyView = mock(CacheTopologyView.class);
+ CacheEngineView l1 = mock(CacheEngineView.class);
+ Cacheable dataBlock = block(BlockType.DATA);
+ CacheWriteContext context = mock(CacheWriteContext.class);
+
+ when(topologyView.getType()).thenReturn(CacheTopologyType.TIERED_EXCLUSIVE);
+ when(topologyView.getEngine(CacheTier.L1)).thenReturn(Optional.of(l1));
+ when(topologyView.getEngine(CacheTier.L2)).thenReturn(Optional.empty());
+
+ applyPressure(policy, CacheTier.L1, topologyView, 3_200L);
+
+ TierDecision decision =
+ policy.selectTier(new BlockCacheKey("file", 99L), dataBlock, context, topologyView);
+
+ assertTrue(decision.isEmpty());
+ }
+
+ /**
+ * Creates an AdaptiveLRU policy with explicitly controlled adaptation parameters.
+ * @param heavyEvictionCountLimit number of heavy periods tolerated before adaptation
+ * @param heavyEvictionMbSizeLimit target eviction volume in megabytes
+ * @param heavyEvictionOverheadCoefficient coefficient controlling admission adjustment speed
+ * @return configured AdaptiveLRU policy
+ */
+ private AdaptiveLruCachePlacementAdmissionPolicy createPolicy(int heavyEvictionCountLimit,
+ long heavyEvictionMbSizeLimit, float heavyEvictionOverheadCoefficient) {
+ Configuration conf = new Configuration(false);
+ conf.setInt(HEAVY_EVICTION_COUNT_LIMIT, heavyEvictionCountLimit);
+ conf.setLong(HEAVY_EVICTION_MB_SIZE_LIMIT, heavyEvictionMbSizeLimit);
+ conf.setFloat(HEAVY_EVICTION_OVERHEAD_COEFFICIENT, heavyEvictionOverheadCoefficient);
+ return new AdaptiveLruCachePlacementAdmissionPolicy(conf);
+ }
+
+ /**
+ * Advances one evaluation period and reports the specified eviction pressure.
+ * @param policy policy receiving the pressure event
+ * @param sourceTier tier reporting pressure
+ * @param topologyView topology view associated with the policy
+ * @param pressureMb cache pressure in megabytes
+ */
+ private void applyPressure(AdaptiveLruCachePlacementAdmissionPolicy policy, CacheTier sourceTier,
+ CacheTopologyView topologyView, long pressureMb) {
+ currentTime += EVALUATION_PERIOD_MS;
+ environmentEdge.setValue(currentTime);
+ policy.onEvictionPressure(sourceTier, pressureMb * MB, topologyView);
+ }
+
+ /**
+ * Creates a mocked single-tier topology view.
+ * @return single-tier topology view
+ */
+ private CacheTopologyView singleTierView() {
+ CacheTopologyView topologyView = mock(CacheTopologyView.class);
+ when(topologyView.getType()).thenReturn(CacheTopologyType.SINGLE_TIER);
+ return topologyView;
+ }
+
+ /**
+ * Creates a mocked tiered-inclusive topology view.
+ * @return tiered-inclusive topology view
+ */
+ private CacheTopologyView tieredInclusiveView() {
+ CacheTopologyView topologyView = mock(CacheTopologyView.class);
+ when(topologyView.getType()).thenReturn(CacheTopologyType.TIERED_INCLUSIVE);
+ return topologyView;
+ }
+
+ /**
+ * Creates a mocked tiered-exclusive topology view with both tiers available.
+ * @return tiered-exclusive topology view
+ */
+ private CacheTopologyView tieredExclusiveView() {
+ CacheTopologyView topologyView = mock(CacheTopologyView.class);
+ CacheEngineView l1 = mock(CacheEngineView.class);
+ CacheEngineView l2 = mock(CacheEngineView.class);
+
+ when(topologyView.getType()).thenReturn(CacheTopologyType.TIERED_EXCLUSIVE);
+ when(topologyView.getEngine(CacheTier.L1)).thenReturn(Optional.of(l1));
+ when(topologyView.getEngine(CacheTier.L2)).thenReturn(Optional.of(l2));
+ return topologyView;
+ }
+
+ /**
+ * Creates a cacheable block with the specified type.
+ * @param blockType block type
+ * @return mocked cacheable block
+ */
+ private Cacheable block(BlockType blockType) {
+ Cacheable block = mock(Cacheable.class);
+ when(block.getBlockType()).thenReturn(blockType);
+ return block;
+ }
+}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestLruCacheEngine.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestLruCacheEngine.java
new file mode 100644
index 000000000000..89ef91a62e97
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestLruCacheEngine.java
@@ -0,0 +1,86 @@
+/*
+ * 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.hadoop.hbase.io.hfile.cache;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.testclassification.IOTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link LruCacheEngine}.
+ */
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestLruCacheEngine {
+
+ private static final String LRU_MIN_FACTOR_CONFIG_NAME = "hbase.lru.blockcache.min.factor";
+
+ /**
+ * Verifies that an eviction pass with no excess occupancy reports zero cache pressure.
+ */
+ @Test
+ void testEvictionPassReportsZeroPressureWhenBelowTargetSize() {
+ LruCacheEngine engine = new LruCacheEngine(1024L * 1024L, 64L * 1024L, false);
+ CacheEvictionListener listener = mock(CacheEvictionListener.class);
+
+ try {
+ engine.setEvictionListener(listener);
+
+ engine.evict();
+
+ verify(listener).onEvictionPressure(engine, 0L);
+ } finally {
+ engine.shutdown();
+ }
+ }
+
+ /**
+ * Verifies that an eviction pass reports the number of bytes the engine determines should be
+ * freed to reach its configured minimum target size.
+ */
+ @Test
+ void testEvictionPassReportsRequestedPressure() {
+ Configuration conf = new Configuration(false);
+ conf.setFloat(LRU_MIN_FACTOR_CONFIG_NAME, 0.5f);
+
+ LruCacheEngine engine = new LruCacheEngine(16L * 1024L * 1024L, 64L * 1024L, false, conf);
+ CacheEvictionListener listener = mock(CacheEvictionListener.class);
+
+ try {
+ long currentSize = engine.getCurrentSize();
+ long newMaxSize = Math.max(1L, currentSize / 2L);
+ long expectedBytesToFree = currentSize - (long) Math.floor(newMaxSize * 0.5f);
+
+ assertTrue(currentSize > 0);
+ assertTrue(expectedBytesToFree > 0);
+
+ engine.setEvictionListener(listener);
+ engine.setMaxSize(newMaxSize);
+
+ verify(listener).onEvictionPressure(engine, expectedBytesToFree);
+ } finally {
+ engine.shutdown();
+ }
+ }
+}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java
index f8019f45f7f6..23feb2b54a45 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessService.java
@@ -45,6 +45,7 @@
import org.apache.hadoop.hbase.testclassification.SmallTests;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
/**
* Tests for {@link TopologyBackedCacheAccessService}.
@@ -629,6 +630,61 @@ public void testRestorePersistentState() throws IOException {
verify(input).close();
}
+ /**
+ * Verifies that cache-pressure feedback from an engine is forwarded to the policy with the
+ * logical tier associated with that engine.
+ */
+ @Test
+ void testEvictionPressureIsForwardedToPolicyWithSourceTier() {
+ CacheTopology topology = mock(CacheTopology.class);
+ CacheTopologyView topologyView = mock(CacheTopologyView.class);
+ CachePlacementAdmissionPolicy policy = mock(CachePlacementAdmissionPolicy.class);
+ CacheEngine l1 = mock(CacheEngine.class);
+ CacheEngine l2 = mock(CacheEngine.class);
+
+ when(topology.getView()).thenReturn(topologyView);
+ when(topology.getTiers()).thenReturn(List.of(CacheTier.L1, CacheTier.L2));
+ when(topology.getEngine(CacheTier.L1)).thenReturn(Optional.of(l1));
+ when(topology.getEngine(CacheTier.L2)).thenReturn(Optional.of(l2));
+
+ new TopologyBackedCacheAccessService(topology, policy);
+
+ ArgumentCaptor