This is an automated email from the ASF dual-hosted git repository.

taklwu pushed a commit to branch HBASE-30018
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/HBASE-30018 by this push:
     new 714ab998426 HBASE-30305 Replace CombinedBlockCache orchestration with 
TieredExclusiveTopology-backed CacheAccessService (#8522)
714ab998426 is described below

commit 714ab998426bfe524e02b3d29441dfa0a435e349
Author: Vladimir Rodionov <[email protected]>
AuthorDate: Mon Aug 10 15:07:09 2026 -0700

    HBASE-30305 Replace CombinedBlockCache orchestration with 
TieredExclusiveTopology-backed CacheAccessService (#8522)
    
    Signed-off-by: Tak Lon (Stephen) Wu <[email protected]>
---
 .../apache/hadoop/hbase/io/hfile/CacheConfig.java  |  17 ++-
 .../cache/BlockCacheBackedCacheAccessService.java  |   7 +
 .../hfile/cache/BlockCacheBackedCacheEngine.java   |  40 ++++-
 .../hbase/io/hfile/cache/CacheAccessService.java   |  38 ++++-
 .../hbase/io/hfile/cache/CacheAccessServices.java  |  50 ++++--
 .../hadoop/hbase/io/hfile/cache/CacheEngine.java   |  29 ++++
 .../hadoop/hbase/io/hfile/cache/TierDecision.java  |   5 +
 .../cache/TopologyBackedCacheAccessService.java    | 169 ++++++++++++++++++++-
 .../cache/TopologyBackedCacheAccessServices.java   |  72 ++++++++-
 .../TestAvoidCellReferencesIntoShippedBlocks.java  |  30 ++--
 .../hadoop/hbase/io/hfile/TestCacheConfig.java     | 126 ++++++++++-----
 .../apache/hadoop/hbase/io/hfile/TestHFile.java    |   2 +-
 .../hfile/cache/CacheAccessServiceTestFactory.java |  94 ++++++++++++
 ...sServices.java => TestCacheAccessServices.java} |  54 ++++---
 ...CompatibleTopologyBackedCacheAccessService.java |  17 ++-
 ...heAccessServiceWithBlockCacheBackedEngines.java |  15 +-
 .../TestTopologyBackedCacheAccessServices.java     |  54 +++++++
 .../hbase/regionserver/TestDataTieringManager.java |   6 -
 18 files changed, 697 insertions(+), 128 deletions(-)

diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/CacheConfig.java 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/CacheConfig.java
index a90bc700b5d..fa69b77f5ec 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/CacheConfig.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/CacheConfig.java
@@ -27,6 +27,8 @@ import 
org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory;
 import 
org.apache.hadoop.hbase.io.hfile.cache.BlockCacheBackedCacheAccessService;
 import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessService;
 import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessServices;
+import org.apache.hadoop.hbase.io.hfile.cache.CacheTopologyType;
+import org.apache.hadoop.hbase.io.hfile.cache.TopologyBackedCacheAccessService;
 import org.apache.yetus.audience.InterfaceAudience;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -527,7 +529,20 @@ public class CacheConfig implements 
PropagatingConfigurationObserver {
   }
 
   public boolean isCombinedBlockCache() {
-    return blockCache instanceof CombinedBlockCache;
+    if (blockCache instanceof CombinedBlockCache) {
+      return true;
+    }
+    return isCombinedBlockCacheCompatible(cacheAccessService);
+  }
+
+  private static boolean isCombinedBlockCacheCompatible(CacheAccessService 
cacheAccessService) {
+    if (!(cacheAccessService instanceof TopologyBackedCacheAccessService)) {
+      return false;
+    }
+
+    TopologyBackedCacheAccessService service =
+      (TopologyBackedCacheAccessService) cacheAccessService;
+    return service.getTopology().getType() == 
CacheTopologyType.TIERED_EXCLUSIVE;
   }
 
   public ByteBuffAllocator getByteBuffAllocator() {
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheAccessService.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheAccessService.java
index e55b03f6fd8..aa8f447a055 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheAccessService.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheAccessService.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.hbase.io.hfile.cache;
 
 import java.util.Iterator;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import org.apache.hadoop.conf.Configuration;
@@ -30,6 +31,7 @@ import org.apache.hadoop.hbase.io.hfile.Cacheable;
 import org.apache.hadoop.hbase.io.hfile.CachedBlock;
 import org.apache.hadoop.hbase.io.hfile.HFileBlock;
 import org.apache.hadoop.hbase.io.hfile.HFileInfo;
+import org.apache.hadoop.hbase.util.Pair;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -310,6 +312,11 @@ public class BlockCacheBackedCacheAccessService
     blockCache.notifyFileCachingCompleted(fileName, totalBlockCount, 
dataBlockCount, size);
   }
 
+  @Override
+  public Optional<Map<String, Pair<String, Long>>> getFullyCachedFiles() {
+    return blockCache.getFullyCachedFiles();
+  }
+
   @Override
   public Optional<Boolean> shouldCacheFile(HFileInfo hFileInfo, Configuration 
conf) {
     Objects.requireNonNull(hFileInfo, "hFileInfo must not be null");
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheEngine.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheEngine.java
index 98fca26b46e..840fd9380c0 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheEngine.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheEngine.java
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.hbase.io.hfile.cache;
 
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import org.apache.hadoop.conf.Configuration;
@@ -26,7 +27,11 @@ import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
 import org.apache.hadoop.hbase.io.hfile.BlockType;
 import org.apache.hadoop.hbase.io.hfile.CacheStats;
 import org.apache.hadoop.hbase.io.hfile.Cacheable;
+import org.apache.hadoop.hbase.io.hfile.CachedBlock;
+import org.apache.hadoop.hbase.io.hfile.FirstLevelBlockCache;
 import org.apache.hadoop.hbase.io.hfile.HFileBlock;
+import org.apache.hadoop.hbase.io.hfile.HFileInfo;
+import org.apache.hadoop.hbase.util.Pair;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -184,9 +189,13 @@ public class BlockCacheBackedCacheEngine implements 
CacheEngine {
   }
 
   @Override
-  public Optional<Boolean> isAlreadyCached(BlockCacheKey key) {
-    Objects.requireNonNull(key, "key must not be null");
-    return blockCache.isAlreadyCached(key);
+  public Optional<Boolean> isAlreadyCached(BlockCacheKey cacheKey) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    if (blockCache instanceof FirstLevelBlockCache) {
+      FirstLevelBlockCache firstLevelBlockCache = (FirstLevelBlockCache) 
blockCache;
+      return Optional.of(firstLevelBlockCache.containsBlock(cacheKey));
+    }
+    return blockCache.isAlreadyCached(cacheKey);
   }
 
   @Override
@@ -217,4 +226,29 @@ public class BlockCacheBackedCacheEngine implements 
CacheEngine {
     Objects.requireNonNull(fileName, "fileName must not be null");
     blockCache.notifyFileCachingCompleted(fileName, totalBlockCount, 
dataBlockCount, size);
   }
+
+  @Override
+  public Optional<Boolean> shouldCacheFile(HFileInfo hFileInfo, Configuration 
conf) {
+    Objects.requireNonNull(hFileInfo, "hFileInfo must not be null");
+    Objects.requireNonNull(conf, "conf must not be null");
+    return blockCache.shouldCacheFile(hFileInfo, conf);
+  }
+
+  @Override
+  public Optional<Boolean> shouldCacheBlock(BlockCacheKey key, long 
maxTimeStamp,
+    Configuration conf) {
+    Objects.requireNonNull(key, "key must not be null");
+    Objects.requireNonNull(conf, "conf must not be null");
+    return blockCache.shouldCacheBlock(key, maxTimeStamp, conf);
+  }
+
+  @Override
+  public Optional<Map<String, Pair<String, Long>>> getFullyCachedFiles() {
+    return blockCache.getFullyCachedFiles();
+  }
+
+  @Override
+  public Optional<Iterable<CachedBlock>> asCachedBlockIterable() {
+    return Optional.of(blockCache);
+  }
 }
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessService.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessService.java
index d5dfa54ea27..ceadedcd6dd 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessService.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessService.java
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.hbase.io.hfile.cache;
 
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Consumer;
@@ -29,6 +30,7 @@ import org.apache.hadoop.hbase.io.hfile.CacheStats;
 import org.apache.hadoop.hbase.io.hfile.Cacheable;
 import org.apache.hadoop.hbase.io.hfile.HFileBlock;
 import org.apache.hadoop.hbase.io.hfile.HFileInfo;
+import org.apache.hadoop.hbase.util.Pair;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -123,6 +125,7 @@ public interface CacheAccessService extends 
ConfigurationObserver {
    */
   default Cacheable getBlock(BlockCacheKey cacheKey, boolean caching, boolean 
repeat,
     boolean updateCacheMetrics) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
     CacheRequestContext context = 
CacheRequestContext.newBuilder().withCaching(caching)
       .withRepeat(repeat).withUpdateCacheMetrics(updateCacheMetrics).build();
     return getBlock(cacheKey, context);
@@ -144,6 +147,7 @@ public interface CacheAccessService extends 
ConfigurationObserver {
    */
   default Cacheable getBlock(BlockCacheKey cacheKey, boolean caching, boolean 
repeat,
     boolean updateCacheMetrics, BlockType blockType) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
     CacheRequestContext context =
       CacheRequestContext.newBuilder().withCaching(caching).withRepeat(repeat)
         
.withUpdateCacheMetrics(updateCacheMetrics).withBlockType(blockType).build();
@@ -183,7 +187,10 @@ public interface CacheAccessService extends 
ConfigurationObserver {
    * @param inMemory whether the block should be treated as in-memory
    */
   default void cacheBlock(BlockCacheKey cacheKey, Cacheable block, boolean 
inMemory) {
-    CacheWriteContext context = 
CacheWriteContext.newBuilder().withInMemory(inMemory).build();
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(block, "block must not be null");
+    CacheWriteContext context = 
CacheWriteContext.newBuilder().withInMemory(inMemory)
+      .withBlockCategory(block.getBlockType().getCategory()).build();
     cacheBlock(cacheKey, block, context);
   }
 
@@ -199,10 +206,14 @@ public interface CacheAccessService extends 
ConfigurationObserver {
    * @param inMemory      whether the block should be treated as in-memory
    * @param waitWhenCache whether to wait for the cache operation to be 
accepted/flushed
    */
+
   default void cacheBlock(BlockCacheKey cacheKey, Cacheable block, boolean 
inMemory,
     boolean waitWhenCache) {
-    CacheWriteContext context = 
CacheWriteContext.newBuilder().withInMemory(inMemory)
-      .withWaitWhenCache(waitWhenCache).build();
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(block, "block must not be null");
+    CacheWriteContext context =
+      
CacheWriteContext.newBuilder().withInMemory(inMemory).withWaitWhenCache(waitWhenCache)
+        .withBlockCategory(block.getBlockType().getCategory()).build();
     cacheBlock(cacheKey, block, context);
   }
 
@@ -215,7 +226,11 @@ public interface CacheAccessService extends 
ConfigurationObserver {
    * @param block    block contents
    */
   default void cacheBlock(BlockCacheKey cacheKey, Cacheable block) {
-    cacheBlock(cacheKey, block, CacheWriteContext.newBuilder().build());
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(block, "block must not be null");
+    CacheWriteContext context =
+      
CacheWriteContext.newBuilder().withBlockCategory(block.getBlockType().getCategory()).build();
+    cacheBlock(cacheKey, block, context);
   }
 
   /**
@@ -524,4 +539,19 @@ public interface CacheAccessService extends 
ConfigurationObserver {
     Configuration conf) {
     return Optional.empty();
   }
+
+  /**
+   * Returns the files that are fully cached by this cache implementation.
+   * <p>
+   * A file is considered fully cached when all of its cacheable blocks are 
present in the cache.
+   * Not all cache implementations track this information. Implementations 
that do not support this
+   * capability should return {@link Optional#empty()}.
+   * </p>
+   * @return an {@link Optional} containing a map of fully cached files when 
this capability is
+   *         supported; otherwise {@link Optional#empty()}
+   */
+  default Optional<Map<String, Pair<String, Long>>> getFullyCachedFiles() {
+    return Optional.empty();
+  }
+
 }
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 d4e739fcdcc..a020323737a 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
@@ -23,6 +23,7 @@ import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.io.hfile.BlockCache;
 import org.apache.hadoop.hbase.io.hfile.BlockCacheFactory;
 import org.apache.hadoop.hbase.io.hfile.CachedBlock;
+import org.apache.hadoop.hbase.io.hfile.CombinedBlockCache;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -46,18 +47,26 @@ public final class CacheAccessServices {
   }
 
   /**
-   * Creates a {@link CacheAccessService} backed by an existing {@link 
BlockCache}.
+   * Creates a cache access service backed by an existing block cache.
    * <p>
-   * This is the default compatibility path during migration from {@code 
BlockCache} to
-   * {@code CacheAccessService}. The returned service delegates to the 
supplied block cache and
-   * should preserve existing behavior.
+   * For regular {@link BlockCache} implementations, this returns a legacy
+   * {@link BlockCacheBackedCacheAccessService}. For {@link 
CombinedBlockCache}, this returns a
+   * topology-backed service using {@link TieredExclusiveTopology}. This moves 
combined L1/L2
+   * orchestration to the new topology layer while keeping the existing 
combined block cache object
+   * available for legacy {@link BlockCache}-facing APIs.
    * </p>
-   * @param blockCache block cache to wrap
-   * @return cache access service backed by {@code blockCache}
+   * @param blockCache block cache to expose through {@link CacheAccessService}
+   * @return cache access service
    */
+
   public static CacheAccessService fromBlockCache(BlockCache blockCache) {
-    return new BlockCacheBackedCacheAccessService(
-      Objects.requireNonNull(blockCache, "blockCache must not be null"));
+    Objects.requireNonNull(blockCache, "blockCache must not be null");
+    if (blockCache instanceof CombinedBlockCache) {
+      return TopologyBackedCacheAccessServices
+        .fromCombinedBlockCache((CombinedBlockCache) blockCache);
+    }
+    return new BlockCacheBackedCacheAccessService(blockCache);
+
   }
 
   /**
@@ -132,13 +141,26 @@ public final class CacheAccessServices {
    * @throws NullPointerException if {@code cacheAccessService} is {@code null}
    */
   @SuppressWarnings("unchecked")
-  public static Optional<Iterable<CachedBlock>>
-    asCachedBlockIterable(CacheAccessService cacheAccessService) {
-    Objects.requireNonNull(cacheAccessService, "cacheAccessService must not be 
null");
-    if (cacheAccessService instanceof Iterable) {
-      return Optional.of((Iterable<CachedBlock>) cacheAccessService);
+  // public static Optional<Iterable<CachedBlock>>
+  // asCachedBlockIterable(CacheAccessService cacheAccessService) {
+  // Objects.requireNonNull(cacheAccessService, "cacheAccessService must not 
be null");
+  // if (cacheAccessService instanceof Iterable) {
+  // return Optional.of((Iterable<CachedBlock>) cacheAccessService);
+  // }
+  // return Optional.empty();
+  // }
+
+  public static Optional<Iterable<CachedBlock>> 
asCachedBlockIterable(CacheAccessService service) {
+    Objects.requireNonNull(service, "service must not be null");
+
+    if (service instanceof TopologyBackedCacheAccessService) {
+      return ((TopologyBackedCacheAccessService) 
service).asCachedBlockIterable();
     }
+
+    if (service instanceof BlockCacheBackedCacheAccessService) {
+      return Optional.of(((BlockCacheBackedCacheAccessService) 
service).getBlockCache());
+    }
+
     return Optional.empty();
   }
-
 }
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEngine.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEngine.java
index b5564657dfb..270cbc24e1b 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEngine.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEngine.java
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.hbase.io.hfile.cache;
 
+import java.util.Map;
 import java.util.Optional;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.Path;
@@ -24,7 +25,10 @@ import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
 import org.apache.hadoop.hbase.io.hfile.BlockType;
 import org.apache.hadoop.hbase.io.hfile.CacheStats;
 import org.apache.hadoop.hbase.io.hfile.Cacheable;
+import org.apache.hadoop.hbase.io.hfile.CachedBlock;
 import org.apache.hadoop.hbase.io.hfile.HFileBlock;
+import org.apache.hadoop.hbase.io.hfile.HFileInfo;
+import org.apache.hadoop.hbase.util.Pair;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -302,4 +306,29 @@ public interface CacheEngine {
     long size) {
     // noop
   }
+
+  default Optional<Boolean> shouldCacheFile(HFileInfo hFileInfo, Configuration 
conf) {
+    return Optional.empty();
+  }
+
+  default Optional<Boolean> shouldCacheBlock(BlockCacheKey key, long 
maxTimeStamp,
+    Configuration conf) {
+    return Optional.empty();
+  }
+
+  default Optional<Map<String, Pair<String, Long>>> getFullyCachedFiles() {
+    return Optional.empty();
+  }
+
+  /**
+   * Returns an iterable view of cached blocks exposed by this cache engine.
+   * <p>
+   * Not all cache engines expose their internal cached block list. 
Implementations that do not
+   * support this capability should return {@link Optional#empty()}.
+   * </p>
+   * @return an iterable over cached blocks when supported; otherwise {@link 
Optional#empty()}
+   */
+  default Optional<Iterable<CachedBlock>> asCachedBlockIterable() {
+    return Optional.empty();
+  }
 }
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TierDecision.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TierDecision.java
index fb0434b0ff7..d45151c093f 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TierDecision.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TierDecision.java
@@ -112,4 +112,9 @@ public final class TierDecision {
   public boolean isEmpty() {
     return tiers.isEmpty();
   }
+
+  @Override
+  public String toString() {
+    return "TierDecision{" + "tiers=" + tiers + '}';
+  }
 }
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 b7c278d3ead..828368274e4 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
@@ -17,14 +17,23 @@
  */
 package org.apache.hadoop.hbase.io.hfile.cache;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.StreamSupport;
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
 import org.apache.hadoop.hbase.io.hfile.BlockType;
 import org.apache.hadoop.hbase.io.hfile.CacheStats;
 import org.apache.hadoop.hbase.io.hfile.Cacheable;
+import org.apache.hadoop.hbase.io.hfile.CachedBlock;
 import org.apache.hadoop.hbase.io.hfile.HFileBlock;
+import org.apache.hadoop.hbase.io.hfile.HFileInfo;
+import org.apache.hadoop.hbase.util.Pair;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -121,9 +130,59 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
    * @return cached block, or {@code null} if not present in any tier
    */
   @Override
+
   public Cacheable getBlock(BlockCacheKey cacheKey, CacheRequestContext 
context) {
     Objects.requireNonNull(cacheKey, "cacheKey must not be null");
     Objects.requireNonNull(context, "context must not be null");
+    if (topology.getType() == CacheTopologyType.TIERED_EXCLUSIVE) {
+      return getBlockFromTieredExclusiveTopology(cacheKey, context);
+    }
+    return getBlockFromAllTiers(cacheKey, context);
+
+  }
+
+  private Cacheable getBlockFromTieredExclusiveTopology(BlockCacheKey cacheKey,
+    CacheRequestContext context) {
+    Optional<CacheEngine> l1 = topology.getEngine(CacheTier.L1);
+    Optional<CacheEngine> l2 = topology.getEngine(CacheTier.L2);
+
+    if (!l1.isPresent() && !l2.isPresent()) {
+      return null;
+    }
+
+    if (!l1.isPresent()) {
+      return getBlockFromEngine(l2.get(), cacheKey, context);
+    }
+
+    if (!l2.isPresent()) {
+      return getBlockFromEngine(l1.get(), cacheKey, context);
+    }
+
+    CacheEngine selectedEngine = l1.get();
+    CacheTier selectedTier = CacheTier.L1;
+
+    Optional<Boolean> existsInL1 = l1.get().isAlreadyCached(cacheKey);
+    if (!existsInL1.orElse(false)) {
+      selectedEngine = l2.get();
+      selectedTier = CacheTier.L2;
+    }
+
+    Cacheable block = getBlockFromEngine(selectedEngine, cacheKey, context);
+    boolean updateCacheMetrics = context.isUpdateCacheMetrics();
+    boolean caching = context.isCaching();
+    if (updateCacheMetrics) {
+      updateBlockMetrics(block, cacheKey, selectedEngine, caching);
+    }
+
+    if (block != null) {
+      maybePromote(cacheKey, block, selectedTier, selectedEngine, context);
+    }
+    return block;
+  }
+
+  private Cacheable getBlockFromAllTiers(BlockCacheKey cacheKey, 
CacheRequestContext context) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(context, "context must not be null");
 
     for (CacheTier tier : topology.getTiers()) {
       Optional<CacheEngine> engine = topology.getEngine(tier);
@@ -141,6 +200,19 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
     return null;
   }
 
+  private void updateBlockMetrics(Cacheable block, BlockCacheKey key, 
CacheEngine engine,
+    boolean caching) {
+    CacheStats stats = engine.getStats();
+    if (stats == null) {
+      return;
+    }
+    if (block == null) {
+      stats.miss(caching, key.isPrimary(), key.getBlockType());
+    } else {
+      stats.hit(caching, key.isPrimary(), key.getBlockType());
+    }
+  }
+
   /**
    * Adds a block to the cache using policy-selected target tiers.
    * <p>
@@ -156,6 +228,7 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
    * @param block    block contents
    * @param context  cache write context
    */
+
   @Override
   public void cacheBlock(BlockCacheKey cacheKey, Cacheable block, 
CacheWriteContext context) {
     Objects.requireNonNull(cacheKey, "cacheKey must not be null");
@@ -177,15 +250,28 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
     }
   }
 
-  /**
-   * Evicts a single block from all engines participating in the topology.
-   * @param cacheKey block to remove
-   * @return {@code true} if at least one engine removed the block, {@code 
false} otherwise
-   */
   @Override
   public boolean evictBlock(BlockCacheKey cacheKey) {
     Objects.requireNonNull(cacheKey, "cacheKey must not be null");
 
+    if (topology.getType() == CacheTopologyType.TIERED_EXCLUSIVE) {
+      return evictBlockFromFirstMatchingTier(cacheKey);
+    }
+
+    return evictBlockFromAllTiers(cacheKey);
+  }
+
+  private boolean evictBlockFromFirstMatchingTier(BlockCacheKey cacheKey) {
+    for (CacheTier tier : topology.getTiers()) {
+      Optional<CacheEngine> engine = topology.getEngine(tier);
+      if (engine.isPresent() && engine.get().evictBlock(cacheKey)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean evictBlockFromAllTiers(BlockCacheKey cacheKey) {
     boolean evicted = false;
     for (CacheEngine engine : topology.getEngines()) {
       evicted |= engine.evictBlock(cacheKey);
@@ -456,6 +542,37 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
     }
   }
 
+  @Override
+  public Optional<Boolean> shouldCacheFile(HFileInfo hFileInfo, Configuration 
conf) {
+    Objects.requireNonNull(hFileInfo, "hFileInfo must not be null");
+    Objects.requireNonNull(conf, "conf must not be null");
+
+    boolean shouldCache = true;
+    for (CacheEngine engine : topology.getEngines()) {
+      Optional<Boolean> result = engine.shouldCacheFile(hFileInfo, conf);
+      if (result.isPresent()) {
+        shouldCache = shouldCache && result.get();
+      }
+    }
+    return Optional.of(shouldCache);
+  }
+
+  @Override
+  public Optional<Boolean> shouldCacheBlock(BlockCacheKey key, long 
maxTimeStamp,
+    Configuration conf) {
+    Objects.requireNonNull(key, "key must not be null");
+    Objects.requireNonNull(conf, "conf must not be null");
+
+    boolean shouldCache = true;
+    for (CacheEngine engine : topology.getEngines()) {
+      Optional<Boolean> result = engine.shouldCacheBlock(key, maxTimeStamp, 
conf);
+      if (result.isPresent()) {
+        shouldCache = shouldCache && result.get();
+      }
+    }
+    return Optional.of(shouldCache);
+  }
+
   private Cacheable getBlockFromEngine(CacheEngine engine, BlockCacheKey 
cacheKey,
     CacheRequestContext context) {
     Optional<BlockType> blockType = context.getBlockType();
@@ -483,4 +600,46 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
 
     topology.promote(cacheKey, block, sourceEngine, targetEngine.get());
   }
+
+  @Override
+  public Optional<Map<String, Pair<String, Long>>> getFullyCachedFiles() {
+    Map<String, Pair<String, Long>> fullyCachedFiles = new HashMap<>();
+    boolean found = false;
+
+    for (CacheEngine engine : topology.getEngines()) {
+      Optional<Map<String, Pair<String, Long>>> result = 
engine.getFullyCachedFiles();
+      if (result.isPresent()) {
+        found = true;
+        fullyCachedFiles.putAll(result.get());
+      }
+    }
+
+    return found ? Optional.of(fullyCachedFiles) : Optional.empty();
+  }
+
+  @Override
+  public void notifyFileCachingCompleted(Path path, int blockCount, int 
dataBlockCount, long size) {
+    Objects.requireNonNull(path, "path must not be null");
+
+    for (CacheEngine engine : topology.getEngines()) {
+      engine.notifyFileCachingCompleted(path, blockCount, dataBlockCount, 
size);
+    }
+  }
+
+  public Optional<Iterable<CachedBlock>> asCachedBlockIterable() {
+    List<Iterable<CachedBlock>> iterables = new ArrayList<>();
+
+    for (CacheEngine engine : topology.getEngines()) {
+      engine.asCachedBlockIterable().ifPresent(iterables::add);
+    }
+
+    if (iterables.isEmpty()) {
+      return Optional.empty();
+    }
+
+    Iterable<CachedBlock> cachedBlocks = () -> iterables.stream()
+      .flatMap(iterable -> StreamSupport.stream(iterable.spliterator(), 
false)).iterator();
+
+    return Optional.of(cachedBlocks);
+  }
 }
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessServices.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessServices.java
index 60b2b9f6ddf..3ce586c22a7 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessServices.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessServices.java
@@ -19,6 +19,8 @@ package org.apache.hadoop.hbase.io.hfile.cache;
 
 import java.util.Objects;
 import org.apache.hadoop.hbase.io.hfile.BlockCache;
+import org.apache.hadoop.hbase.io.hfile.CombinedBlockCache;
+import org.apache.hadoop.hbase.io.hfile.FirstLevelBlockCache;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -30,17 +32,58 @@ import org.apache.yetus.audience.InterfaceAudience;
  * {@link TieredExclusiveTopology}, and exposed through {@link 
TopologyBackedCacheAccessService}.
  * </p>
  * <p>
- * This class does not change production cache wiring by itself. It only 
provides a reusable
- * construction path for tests and later migration steps that need a 
CombinedBlockCache-compatible
- * topology-backed service.
+ * This class does not create or remove any concrete cache implementation by 
itself. It only
+ * provides a reusable construction path for tests and migration steps that 
need a
+ * CombinedBlockCache-compatible topology-backed service.
  * </p>
  */
 @InterfaceAudience.Private
 public final class TopologyBackedCacheAccessServices {
 
+  private static final int COMBINED_BLOCK_CACHE_TIER_COUNT = 2;
+
   private TopologyBackedCacheAccessServices() {
   }
 
+  /**
+   * Creates a topology-backed cache access service from an existing combined 
block cache.
+   * <p>
+   * The supplied {@link CombinedBlockCache} is used only as a legacy holder 
for the participating
+   * L1 and L2 {@link BlockCache} instances. The returned service uses
+   * {@link TieredExclusiveTopology} as the actual orchestration model.
+   * </p>
+   * @param combinedBlockCache combined block cache containing L1 and L2 caches
+   * @return topology-backed cache access service
+   */
+  public static TopologyBackedCacheAccessService
+    fromCombinedBlockCache(CombinedBlockCache combinedBlockCache) {
+    return fromCombinedBlockCache(combinedBlockCache,
+      new DefaultHBaseCachePlacementAdmissionPolicy());
+  }
+
+  /**
+   * Creates a topology-backed cache access service from an existing combined 
block cache.
+   * <p>
+   * This overload allows tests and future wiring code to provide an explicit 
policy while still
+   * extracting L1 and L2 caches from the supplied {@link CombinedBlockCache}.
+   * </p>
+   * @param combinedBlockCache combined block cache containing L1 and L2 caches
+   * @param policy             placement and admission policy
+   * @return topology-backed cache access service
+   */
+  public static TopologyBackedCacheAccessService fromCombinedBlockCache(
+    CombinedBlockCache combinedBlockCache, CachePlacementAdmissionPolicy 
policy) {
+    Objects.requireNonNull(combinedBlockCache, "combinedBlockCache must not be 
null");
+    Objects.requireNonNull(policy, "policy must not be null");
+
+    BlockCache[] blockCaches = combinedBlockCache.getBlockCaches();
+    if (blockCaches.length != COMBINED_BLOCK_CACHE_TIER_COUNT) {
+      throw new IllegalArgumentException("combinedBlockCache must expose 
exactly two block caches");
+    }
+
+    return fromTieredExclusiveBlockCaches("combined", blockCaches[0], 
blockCaches[1], policy);
+  }
+
   /**
    * Creates a topology-backed cache access service from existing L1 and L2 
block caches.
    * <p>
@@ -60,10 +103,31 @@ public final class TopologyBackedCacheAccessServices {
     Objects.requireNonNull(l1, "l1 must not be null");
     Objects.requireNonNull(l2, "l2 must not be null");
     Objects.requireNonNull(policy, "policy must not be null");
-
+    wireVictimCache(l1, l2);
     CacheEngine l1Engine = CacheEngines.fromBlockCache(l1);
     CacheEngine l2Engine = CacheEngines.fromBlockCache(l2);
     CacheTopology topology = new TieredExclusiveTopology(name, l1Engine, 
l2Engine);
     return new TopologyBackedCacheAccessService(topology, policy);
   }
+
+  /**
+   * Configures the legacy L1 to L2 victim-cache relationship used by 
CombinedBlockCache.
+   * <p>
+   * The topology-backed service owns lookup and placement orchestration, but 
existing
+   * {@link FirstLevelBlockCache} implementations still use a direct 
victim-cache reference to move
+   * evicted blocks from L1 to L2. Keep this wiring while L1 and L2 are still 
legacy
+   * {@link BlockCache} implementations.
+   * </p>
+   * @param l1 first-level block cache
+   * @param l2 second-level block cache
+   */
+  private static void wireVictimCache(BlockCache l1, BlockCache l2) {
+    if (l1 instanceof FirstLevelBlockCache) {
+      try {
+        ((FirstLevelBlockCache) l1).setVictimCache(l2);
+      } catch (IllegalArgumentException e) {
+        // ignore if already wired
+      }
+    }
+  }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAvoidCellReferencesIntoShippedBlocks.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAvoidCellReferencesIntoShippedBlocks.java
index 51599c7f0f7..2f048f1034f 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAvoidCellReferencesIntoShippedBlocks.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAvoidCellReferencesIntoShippedBlocks.java
@@ -26,6 +26,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.Cell;
@@ -44,10 +45,9 @@ import org.apache.hadoop.hbase.io.hfile.BlockCache;
 import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
 import org.apache.hadoop.hbase.io.hfile.CacheConfig;
 import org.apache.hadoop.hbase.io.hfile.CachedBlock;
-import org.apache.hadoop.hbase.io.hfile.CombinedBlockCache;
 import org.apache.hadoop.hbase.io.hfile.bucket.BucketCache;
-import 
org.apache.hadoop.hbase.io.hfile.cache.BlockCacheBackedCacheAccessService;
 import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessService;
+import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessServiceTestFactory;
 import org.apache.hadoop.hbase.io.hfile.cache.CacheAccessServices;
 import org.apache.hadoop.hbase.regionserver.DelegatingInternalScanner;
 import org.apache.hadoop.hbase.regionserver.HRegion;
@@ -118,9 +118,6 @@ public class TestAvoidCellReferencesIntoShippedBlocks {
     compactReadLatch = new CountDownLatch(1);
   }
 
-  /**
-   * @throws java.lang.Exception
-   */
   @AfterAll
   public static void tearDownAfterClass() throws Exception {
     TEST_UTIL.shutdownMiniCluster();
@@ -368,7 +365,6 @@ public class TestAvoidCellReferencesIntoShippedBlocks {
       try (ScanPerNextResultScanner scanner =
         new 
ScanPerNextResultScanner(TEST_UTIL.getAsyncConnection().getTable(tableName), 
s)) {
         Thread evictorThread = new Thread() {
-          @SuppressWarnings("unchecked")
           @Override
           public void run() {
             List<BlockCacheKey> cacheList = new ArrayList<>();
@@ -439,7 +435,7 @@ public class TestAvoidCellReferencesIntoShippedBlocks {
           count++;
           if (count == 2) {
             evictorThread.start();
-            latch.await();
+            assertTrue(latch.await(30, TimeUnit.SECONDS), "Evictor thread did 
not complete");
           }
         }
       }
@@ -451,17 +447,15 @@ public class TestAvoidCellReferencesIntoShippedBlocks {
    * For {@link BucketCache},we only evict Block if there is no rpc referenced.
    */
   private void evictBlock(CacheAccessService cache, BlockCacheKey 
blockCacheKey) {
-    // TODO: will be refactored later once we get CacheEngine refactoring done
-    BlockCache blockCache = ((BlockCacheBackedCacheAccessService) 
cache).getBlockCache();
-    assertTrue(blockCache instanceof CombinedBlockCache);
-    BlockCache[] blockCaches = blockCache.getBlockCaches();
-    for (BlockCache currentBlockCache : blockCaches) {
-      if (currentBlockCache instanceof BucketCache) {
-        ((BucketCache) 
currentBlockCache).evictBlockIfNoRpcReferenced(blockCacheKey);
-      } else {
-        currentBlockCache.evictBlock(blockCacheKey);
-      }
-    }
+    BlockCache l1Cache = 
CacheAccessServiceTestFactory.getFirstLevelBlockCache(cache);
+    BlockCache l2Cache = 
CacheAccessServiceTestFactory.getSecondLevelBlockCache(cache);
+
+    l1Cache.evictBlock(blockCacheKey);
 
+    if (l2Cache instanceof BucketCache) {
+      ((BucketCache) l2Cache).evictBlockIfNoRpcReferenced(blockCacheKey);
+    } else {
+      l2Cache.evictBlock(blockCacheKey);
+    }
   }
 }
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 03c9f52d37a..1d7286dbe6c 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
@@ -21,6 +21,8 @@ import static org.junit.Assert.assertSame;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -28,12 +30,14 @@ import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.lang.management.MemoryUsage;
 import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.hadoop.hbase.HBaseTestingUtil;
 import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.Waiter;
 import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
 import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
 import org.apache.hadoop.hbase.io.ByteBuffAllocator;
@@ -48,7 +52,6 @@ import org.apache.hadoop.hbase.nio.ByteBuff;
 import org.apache.hadoop.hbase.testclassification.IOTests;
 import org.apache.hadoop.hbase.testclassification.MediumTests;
 import org.apache.hadoop.hbase.util.Bytes;
-import org.apache.hadoop.hbase.util.Threads;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -104,7 +107,7 @@ public class TestCacheConfig {
   }
 
   static class DataCacheEntry implements Cacheable {
-    private static final int SIZE = 1;
+    private static final int SIZE = 1 << 20; // 1MB
     private static DataCacheEntry SINGLETON = new DataCacheEntry();
     final CacheableDeserializer<Cacheable> deserializer;
 
@@ -307,27 +310,58 @@ public class TestCacheConfig {
     CacheConfig cc = new CacheConfig(this.conf);
     CacheAccessService service = 
CacheAccessServiceTestFactory.fromConfiguration(this.conf);
     basicBlockCacheOps(service, cc, false, false);
-    assertTrue(CacheAccessServiceTestFactory.blockCache(service) instanceof 
CombinedBlockCache);
+    
assertTrue(CacheAccessServiceTestFactory.isCombinedBlockCacheEquivalent(service));
     // TODO: Assert sizes allocated are right and proportions.
-    CombinedBlockCache cbc = (CombinedBlockCache) 
CacheAccessServiceTestFactory.blockCache(service);
-    BlockCache[] bcs = cbc.getBlockCaches();
-    assertTrue(bcs[0] instanceof LruBlockCache);
-    LruBlockCache lbc = (LruBlockCache) bcs[0];
+    LruBlockCache lbc =
+      (LruBlockCache) 
CacheAccessServiceTestFactory.getFirstLevelBlockCache(service);
+    ;
     assertEquals(MemorySizeUtil.getOnHeapCacheSize(this.conf), 
lbc.getMaxSize());
-    assertTrue(bcs[1] instanceof BucketCache);
-    BucketCache bc = (BucketCache) bcs[1];
+    BucketCache bc = (BucketCache) 
CacheAccessServiceTestFactory.getSecondLevelBlockCache(service);
     // getMaxSize comes back in bytes but we specified size in MB
     assertEquals(bcSize, bc.getMaxSize() / (1024 * 1024));
   }
 
   /**
-   * Assert that when BUCKET_CACHE_COMBINED_KEY is false, the non-default, 
that we deploy
-   * LruBlockCache as L1 with a BucketCache for L2.
+   * Verifies the legacy two-tier block cache layout used when bucket cache is 
enabled but the
+   * combined-cache mode is disabled.
+   * <p>
+   * In this configuration HBase should deploy an {@link LruBlockCache} as the 
first-level in-memory
+   * cache and a {@link BucketCache} as the second-level victim cache. Blocks 
are inserted into L1
+   * first. When L1 evicts blocks under memory pressure, the evicted blocks 
should be passed to the
+   * configured L2 victim cache.
+   * </p>
+   * <p>
+   * This test intentionally verifies the L1-to-L2 victim-cache relationship 
without relying on an
+   * exact final L1 block count or on a specific block key being evicted. 
{@link LruBlockCache}
+   * eviction policy does not guarantee which block will be selected for 
eviction, only that some
+   * blocks may be evicted when cache pressure exceeds the configured 
threshold.
+   * </p>
+   * <p>
+   * The previous version of this test attempted to force eviction by 
inserting a single synthetic
+   * block whose size was {@code acceptableSize() + 1}, and then waited until 
the L1 block count
+   * returned to its original value. That approach was flawed for two reasons:
+   * </p>
+   * <ol>
+   * <li>{@link LruBlockCache} rejects any block larger than its maximum 
cacheable block size before
+   * eviction can run. Since {@code acceptableSize()} depends on the JVM heap 
size while the maximum
+   * cacheable block size is fixed by configuration, {@code acceptableSize() + 
1} may be larger than
+   * the maximum cacheable block size. In that case the block is rejected and 
no eviction is
+   * triggered.</li>
+   * <li>If the synthetic block is small enough to be accepted, eviction runs 
only after the block
+   * is inserted. The eviction policy is not required to restore the exact 
previous block count, nor
+   * is it required to evict the originally inserted block. Waiting for an 
exact L1 block count can
+   * therefore hang indefinitely.</li>
+   * </ol>
+   * <p>
+   * The test now creates cache pressure using normal cacheable blocks and 
waits with a timeout
+   * until L2 receives at least one block from L1 eviction. This directly 
verifies the intended
+   * contract: L1 is wired with L2 as its victim cache.
+   * </p>
    */
   @Test
-  public void testBucketCacheConfigL1L2Setup() {
+  public void testBucketCacheConfigL1L2Setup() throws Exception {
     this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap");
-    // Make lru size is smaller than bcSize for sure. Need this to be true so 
when eviction
+    // this.conf.setLong("hbase.lru.max.block.size", 1L << 30);
     // from L1 happens, it does not fail because L2 can't take the eviction 
because block too big.
     this.conf.setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0.001f);
     MemoryUsage mu = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
@@ -339,12 +373,12 @@ public class TestCacheConfig {
     CacheConfig cc = new CacheConfig(this.conf);
     CacheAccessService service = 
CacheAccessServiceTestFactory.fromConfiguration(this.conf);
     basicBlockCacheOps(service, cc, false, false);
-    assertTrue(CacheAccessServiceTestFactory.blockCache(service) instanceof 
CombinedBlockCache);
+    
assertTrue(CacheAccessServiceTestFactory.isCombinedBlockCacheEquivalent(service));
     // TODO: Assert sizes allocated are right and proportions.
-    CombinedBlockCache cbc = (CombinedBlockCache) 
CacheAccessServiceTestFactory.blockCache(service);
-    FirstLevelBlockCache lbc = cbc.l1Cache;
+    FirstLevelBlockCache lbc =
+      (FirstLevelBlockCache) 
CacheAccessServiceTestFactory.getFirstLevelBlockCache(service);
     assertEquals(lruExpectedSize, lbc.getMaxSize());
-    BlockCache bc = cbc.l2Cache;
+    BlockCache bc = 
CacheAccessServiceTestFactory.getSecondLevelBlockCache(service);
     // getMaxSize comes back in bytes but we specified size in MB
     assertEquals(bcExpectedSize, ((BucketCache) bc).getMaxSize());
     // Test the L1+L2 deploy works as we'd expect with blocks evicted from L1 
going to L2.
@@ -355,24 +389,48 @@ public class TestCacheConfig {
     lbc.cacheBlock(bck, c, false);
     assertEquals(initialL1BlockCount + 1, lbc.getBlockCount());
     assertEquals(initialL2BlockCount, bc.getBlockCount());
-    // Force evictions by putting in a block too big.
-    final long justTooBigSize = ((LruBlockCache) lbc).acceptableSize() + 1;
-    lbc.cacheBlock(new BlockCacheKey("bck2", 0), new DataCacheEntry() {
-      @Override
-      public long heapSize() {
-        return justTooBigSize;
-      }
-
-      @Override
-      public int getSerializedLength() {
-        return (int) heapSize();
-      }
+
+    assertNotNull(lbc.getBlock(bck, true, false, true));
+    assertNull(bc.getBlock(bck, true, false, true));
+    waitForAnyBlockToMoveFromL1ToL2(lbc, bc, initialL2BlockCount);
+    assertTrue(bc.getBlockCount() > initialL2BlockCount);
+  }
+
+  /**
+   * Adds cacheable blocks to L1 until L1 eviction moves at least one block 
into L2.
+   * <p>
+   * The helper does not wait for a particular key to appear in L2. {@link 
LruBlockCache} eviction
+   * is policy-driven and does not guarantee that the first inserted block, or 
any specific later
+   * block, will be evicted first. The observable contract needed by this test 
is only that an L1
+   * eviction is forwarded to the configured L2 victim cache.
+   * </p>
+   * <p>
+   * This helper also avoids using a single oversized block to force eviction. 
Oversized blocks may
+   * be rejected by {@link LruBlockCache} before eviction can run. Instead, it 
inserts regular
+   * cacheable blocks and relies on cumulative cache pressure.
+   * </p>
+   * @param l1Cache             first-level cache
+   * @param l2Cache             second-level victim cache
+   * @param initialL2BlockCount L2 block count before creating L1 pressure
+   * @throws Exception if the expected L1-to-L2 movement does not happen 
before the wait timeout
+   */
+  private void waitForAnyBlockToMoveFromL1ToL2(FirstLevelBlockCache l1Cache, 
BlockCache l2Cache,
+    long initialL2BlockCount) throws Exception {
+    AtomicInteger blockIndex = new AtomicInteger();
+
+    /*
+     * Do not try to force eviction with one block of size acceptableSize() + 
1. LruBlockCache
+     * rejects blocks larger than maxBlockSize before eviction can run. For 
accepted blocks,
+     * eviction runs after insertion and does not guarantee which block will 
be evicted. Therefore
+     * this test should not wait for a particular block key to appear in L2. 
The intended contract
+     * is only that an L1 eviction moves some evicted block into the 
configured L2 victim cache.
+     */
+    Waiter.waitFor(this.conf, 10000, () -> {
+      BlockCacheKey evictionKey = new BlockCacheKey("eviction-" + 
blockIndex.getAndIncrement(), 0);
+      l1Cache.cacheBlock(evictionKey, new DataCacheEntry(), false);
+
+      return l2Cache.getBlockCount() > initialL2BlockCount;
     });
-    // The eviction thread in lrublockcache needs to run.
-    while (initialL1BlockCount != lbc.getBlockCount()) {
-      Threads.sleep(10);
-    }
-    assertEquals(initialL1BlockCount, lbc.getBlockCount());
   }
 
   @Test
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestHFile.java 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestHFile.java
index 0168fa7cc69..2c6cf46c5b9 100644
--- a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestHFile.java
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestHFile.java
@@ -353,7 +353,7 @@ public class TestHFile {
     that.set(BLOCKCACHE_POLICY_KEY, l1CachePolicy);
     CacheAccessService bc = 
CacheAccessServiceTestFactory.fromConfiguration(that);
     assertNotNull(bc);
-    assertTrue(CacheAccessServiceTestFactory.blockCache(bc) instanceof 
CombinedBlockCache);
+    
assertTrue(CacheAccessServiceTestFactory.isCombinedBlockCacheEquivalent(bc));
     return bc;
   }
 
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServiceTestFactory.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServiceTestFactory.java
index 7aec8d9d195..37dbc0a9227 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServiceTestFactory.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/CacheAccessServiceTestFactory.java
@@ -669,4 +669,98 @@ public final class CacheAccessServiceTestFactory {
     throw new IllegalArgumentException("CacheAccessService is not backed by a 
legacy BlockCache: "
       + cacheAccessService.getClass().getName());
   }
+
+  /**
+   * Returns whether the supplied cache access service represents a cache 
layout that is equivalent
+   * to the legacy {@link CombinedBlockCache} L1/L2 organization.
+   * <p>
+   * During the block cache migration, {@link CombinedBlockCache} may no 
longer be the object used
+   * by HFile readers and writers directly. The active cache access path may 
instead be a
+   * {@link TopologyBackedCacheAccessService} backed by a {@link 
TieredExclusiveTopology}. That
+   * topology models the same combined-cache style of exclusive L1/L2 
orchestration where the
+   * participating engines correspond to the legacy L1 and L2 block caches.
+   * </p>
+   * <p>
+   * This helper is intentionally based on {@link CacheAccessService}, not on 
the legacy
+   * {@link BlockCache} field, so callers can reason about the active cache 
access path during the
+   * migration away from {@link BlockCache}-centric APIs.
+   * </p>
+   * @param cacheAccessService cache access service to inspect
+   * @return {@code true} when the supplied service represents a 
CombinedBlockCache-compatible
+   *         topology; {@code false} otherwise
+   */
+  public static boolean isCombinedBlockCacheEquivalent(CacheAccessService 
cacheAccessService) {
+    if (!(cacheAccessService instanceof TopologyBackedCacheAccessService)) {
+      return false;
+    }
+
+    TopologyBackedCacheAccessService topologyBackedService =
+      (TopologyBackedCacheAccessService) cacheAccessService;
+    return topologyBackedService.getTopology().getType() == 
CacheTopologyType.TIERED_EXCLUSIVE;
+  }
+
+  /**
+   * Returns the legacy first-level {@link BlockCache} from a topology-backed 
cache access service.
+   * <p>
+   * This helper is intended only for tests that need to verify compatibility 
with the legacy
+   * {@code CombinedBlockCache} L1/L2 layout after the runtime path has moved 
to
+   * {@link TopologyBackedCacheAccessService}. Production code should not 
unwrap
+   * {@link CacheAccessService} back to {@link BlockCache}.
+   * </p>
+   * @param cacheAccessService cache access service to inspect
+   * @return first-level block cache
+   */
+  public static BlockCache getFirstLevelBlockCache(CacheAccessService 
cacheAccessService) {
+    return getBlockCache(cacheAccessService, CacheTier.L1);
+  }
+
+  /**
+   * Returns the legacy second-level {@link BlockCache} from a topology-backed 
cache access service.
+   * <p>
+   * This helper is intended only for tests that need to verify compatibility 
with the legacy
+   * {@code CombinedBlockCache} L1/L2 layout after the runtime path has moved 
to
+   * {@link TopologyBackedCacheAccessService}. Production code should not 
unwrap
+   * {@link CacheAccessService} back to {@link BlockCache}.
+   * </p>
+   * @param cacheAccessService cache access service to inspect
+   * @return second-level block cache
+   */
+  public static BlockCache getSecondLevelBlockCache(CacheAccessService 
cacheAccessService) {
+    return getBlockCache(cacheAccessService, CacheTier.L2);
+  }
+
+  /**
+   * Returns the legacy {@link BlockCache} backing the requested topology tier.
+   * <p>
+   * This method expects the supplied {@link CacheAccessService} to be a
+   * {@link TopologyBackedCacheAccessService} and the requested tier to be 
backed by a
+   * {@link BlockCacheBackedCacheEngine}. It is deliberately strict so tests 
fail clearly when the
+   * cache access service is not using the expected 
CombinedBlockCache-compatible topology-backed
+   * wiring.
+   * </p>
+   * @param cacheAccessService cache access service to inspect
+   * @param tier               topology tier to unwrap
+   * @return block cache backing the requested tier
+   */
+  public static BlockCache getBlockCache(CacheAccessService 
cacheAccessService, CacheTier tier) {
+    Objects.requireNonNull(cacheAccessService, "cacheAccessService must not be 
null");
+    Objects.requireNonNull(tier, "tier must not be null");
+
+    if (!(cacheAccessService instanceof TopologyBackedCacheAccessService)) {
+      throw new IllegalArgumentException(
+        "cacheAccessService must be a TopologyBackedCacheAccessService");
+    }
+
+    TopologyBackedCacheAccessService topologyBackedService =
+      (TopologyBackedCacheAccessService) cacheAccessService;
+    CacheEngine engine = topologyBackedService.getTopology().getEngine(tier)
+      .orElseThrow(() -> new IllegalArgumentException("No cache engine found 
for tier " + tier));
+
+    if (!(engine instanceof BlockCacheBackedCacheEngine)) {
+      throw new IllegalArgumentException(
+        "Cache engine for tier " + tier + " must be a 
BlockCacheBackedCacheEngine");
+    }
+
+    return ((BlockCacheBackedCacheEngine) engine).getBlockCache();
+  }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheAccessServices.java
similarity index 52%
copy from 
hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
copy to 
hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheAccessServices.java
index 8869d30cb55..1c548b9f930 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheAccessServices.java
@@ -17,14 +17,15 @@
  */
 package org.apache.hadoop.hbase.io.hfile.cache;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.util.Optional;
 import org.apache.hadoop.hbase.io.hfile.BlockCache;
+import org.apache.hadoop.hbase.io.hfile.CombinedBlockCache;
 import org.apache.hadoop.hbase.testclassification.IOTests;
 import org.apache.hadoop.hbase.testclassification.SmallTests;
 import org.junit.jupiter.api.Tag;
@@ -32,24 +33,35 @@ import org.junit.jupiter.api.Test;
 
 @Tag(IOTests.TAG)
 @Tag(SmallTests.TAG)
-public class TestTopologyBackedCacheAccessServices {
+public class TestCacheAccessServices {
 
   @Test
-  void testFromTieredExclusiveBlockCachesCreatesExpectedService() {
+  void testFromBlockCacheCreatesBlockCacheBackedServiceForRegularBlockCache() {
+    BlockCache blockCache = mock(BlockCache.class);
+    CacheAccessService service = 
CacheAccessServices.fromBlockCache(blockCache);
+    assertTrue(service instanceof BlockCacheBackedCacheAccessService);
+  }
+
+  @Test
+  void testFromBlockCacheCreatesTopologyBackedServiceForCombinedBlockCache() {
     BlockCache l1 = mock(BlockCache.class);
     BlockCache l2 = mock(BlockCache.class);
-    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    CombinedBlockCache combinedBlockCache = mock(CombinedBlockCache.class);
+
+    when(combinedBlockCache.getBlockCaches()).thenReturn(new BlockCache[] { 
l1, l2 });
+
+    CacheAccessService service = 
CacheAccessServices.fromBlockCache(combinedBlockCache);
 
-    TopologyBackedCacheAccessService service =
-      
TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches("combined", 
l1, l2, policy);
+    assertTrue(service instanceof TopologyBackedCacheAccessService);
 
-    assertEquals("combined", service.getName());
-    assertSame(policy, service.getPolicy());
-    assertTrue(service.getTopology() instanceof TieredExclusiveTopology);
-    assertEquals(CacheTopologyType.TIERED_EXCLUSIVE, 
service.getTopology().getType());
+    TopologyBackedCacheAccessService topologyBackedService =
+      (TopologyBackedCacheAccessService) service;
 
-    Optional<CacheEngine> l1Engine = 
service.getTopology().getEngine(CacheTier.L1);
-    Optional<CacheEngine> l2Engine = 
service.getTopology().getEngine(CacheTier.L2);
+    assertTrue(topologyBackedService.getTopology() instanceof 
TieredExclusiveTopology);
+    assertSame(CacheTopologyType.TIERED_EXCLUSIVE, 
topologyBackedService.getTopology().getType());
+
+    Optional<CacheEngine> l1Engine = 
topologyBackedService.getTopology().getEngine(CacheTier.L1);
+    Optional<CacheEngine> l2Engine = 
topologyBackedService.getTopology().getEngine(CacheTier.L2);
 
     assertTrue(l1Engine.isPresent());
     assertTrue(l2Engine.isPresent());
@@ -60,18 +72,12 @@ public class TestTopologyBackedCacheAccessServices {
   }
 
   @Test
-  void testFromTieredExclusiveBlockCachesRejectsNullArguments() {
-    BlockCache l1 = mock(BlockCache.class);
-    BlockCache l2 = mock(BlockCache.class);
-    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+  void testFromBlockCacheRejectsNull() {
+    assertThrows(NullPointerException.class, () -> 
CacheAccessServices.fromBlockCache(null));
+  }
 
-    assertThrows(NullPointerException.class,
-      () -> 
TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches(null, l1, l2, 
policy));
-    assertThrows(NullPointerException.class, () -> 
TopologyBackedCacheAccessServices
-      .fromTieredExclusiveBlockCaches("combined", null, l2, policy));
-    assertThrows(NullPointerException.class, () -> 
TopologyBackedCacheAccessServices
-      .fromTieredExclusiveBlockCaches("combined", l1, null, policy));
-    assertThrows(NullPointerException.class, () -> 
TopologyBackedCacheAccessServices
-      .fromTieredExclusiveBlockCaches("combined", l1, l2, null));
+  @Test
+  void testDisabledReturnsNoOpService() {
+    assertTrue(CacheAccessServices.disabled() instanceof 
NoOpCacheAccessService);
   }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
index 58db25bb9dc..209ceeae202 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
@@ -30,6 +30,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.Arrays;
+import java.util.Optional;
 import org.apache.hadoop.hbase.io.hfile.BlockCache;
 import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
 import org.apache.hadoop.hbase.io.hfile.Cacheable;
@@ -50,12 +51,14 @@ public class 
TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
     BlockCacheKey key = new BlockCacheKey("file", 1L);
     Cacheable block = mock(Cacheable.class);
 
+    when(l1.isAlreadyCached(key)).thenReturn(Optional.of(true));
     when(l1.getBlock(key, true, false, true)).thenReturn(block);
 
     TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
 
     assertSame(block, service.getBlock(key, requestContext()));
 
+    verify(l1).isAlreadyCached(key);
     verify(l1).getBlock(key, true, false, true);
     verify(l2, never()).getBlock(any(), anyBoolean(), anyBoolean(), 
anyBoolean());
   }
@@ -66,15 +69,14 @@ public class 
TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
     BlockCache l2 = mock(BlockCache.class);
     BlockCacheKey key = new BlockCacheKey("file", 1L);
     Cacheable block = mock(Cacheable.class);
-
+    when(l1.isAlreadyCached(key)).thenReturn(Optional.of(false));
     when(l1.getBlock(key, true, false, true)).thenReturn(null);
     when(l2.getBlock(key, true, false, true)).thenReturn(block);
 
     TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
 
     assertSame(block, service.getBlock(key, requestContext()));
-
-    verify(l1).getBlock(key, true, false, true);
+    verify(l1).isAlreadyCached(key);
     verify(l2).getBlock(key, true, false, true);
   }
 
@@ -83,15 +85,14 @@ public class 
TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
     BlockCache l1 = mock(BlockCache.class);
     BlockCache l2 = mock(BlockCache.class);
     BlockCacheKey key = new BlockCacheKey("file", 1L);
-
+    when(l1.isAlreadyCached(key)).thenReturn(Optional.of(false));
     when(l1.getBlock(key, true, false, true)).thenReturn(null);
     when(l2.getBlock(key, true, false, true)).thenReturn(null);
 
     TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
 
     assertNull(service.getBlock(key, requestContext()));
-
-    verify(l1).getBlock(key, true, false, true);
+    verify(l1).isAlreadyCached(key);
     verify(l2).getBlock(key, true, false, true);
   }
 
@@ -101,7 +102,7 @@ public class 
TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
     BlockCache l2 = mock(BlockCache.class);
     BlockCacheKey key = new BlockCacheKey("file", 1L);
     Cacheable block = mock(Cacheable.class);
-
+    when(l1.isAlreadyCached(key)).thenReturn(Optional.of(false));
     when(l1.getBlock(key, true, false, true)).thenReturn(null);
     when(l2.getBlock(key, true, false, true)).thenReturn(block);
 
@@ -109,7 +110,7 @@ public class 
TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
 
     assertSame(block, service.getBlock(key, requestContext()));
 
-    verify(l1).getBlock(key, true, false, true);
+    verify(l1).isAlreadyCached(key);
     verify(l2).getBlock(key, true, false, true);
     assertCachedExactlyOnce(l1, key, block);
     verify(l2).evictBlock(key);
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines.java
index 02d39623103..38614cb81db 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines.java
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.util.Arrays;
+import java.util.Optional;
 import org.apache.hadoop.hbase.io.hfile.BlockCache;
 import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
 import org.apache.hadoop.hbase.io.hfile.Cacheable;
@@ -45,13 +46,14 @@ public class 
TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines {
     BlockCache l2 = mock(BlockCache.class);
     BlockCacheKey key = new BlockCacheKey("file", 1L);
     Cacheable block = mock(Cacheable.class);
+    when(l1.isAlreadyCached(key)).thenReturn(Optional.of(true));
 
     when(l1.getBlock(key, true, false, true)).thenReturn(block);
 
     TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
 
     assertSame(block, service.getBlock(key, requestContext()));
-
+    verify(l1).isAlreadyCached(key);
     verify(l1).getBlock(key, true, false, true);
     verify(l2, never()).getBlock(any(), any(Boolean.class), 
any(Boolean.class), any(Boolean.class));
   }
@@ -62,7 +64,7 @@ public class 
TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines {
     BlockCache l2 = mock(BlockCache.class);
     BlockCacheKey key = new BlockCacheKey("file", 1L);
     Cacheable block = mock(Cacheable.class);
-
+    when(l1.isAlreadyCached(key)).thenReturn(Optional.of(false));
     when(l1.getBlock(key, true, false, true)).thenReturn(null);
     when(l2.getBlock(key, true, false, true)).thenReturn(block);
 
@@ -70,7 +72,8 @@ public class 
TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines {
 
     assertSame(block, service.getBlock(key, requestContext()));
 
-    verify(l1).getBlock(key, true, false, true);
+    verify(l1, never()).getBlock(any(), any(Boolean.class), 
any(Boolean.class), any(Boolean.class));
+    verify(l1).isAlreadyCached(key);
     verify(l2).getBlock(key, true, false, true);
     verify(l1).cacheBlock(key, block);
     verify(l2).evictBlock(key);
@@ -81,15 +84,15 @@ public class 
TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines {
     BlockCache l1 = mock(BlockCache.class);
     BlockCache l2 = mock(BlockCache.class);
     BlockCacheKey key = new BlockCacheKey("file", 1L);
-
+    when(l1.isAlreadyCached(key)).thenReturn(Optional.of(false));
     when(l1.getBlock(key, true, false, true)).thenReturn(null);
     when(l2.getBlock(key, true, false, true)).thenReturn(null);
 
     TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
 
     assertNull(service.getBlock(key, requestContext()));
-
-    verify(l1).getBlock(key, true, false, true);
+    verify(l1, never()).getBlock(any(), any(Boolean.class), 
any(Boolean.class), any(Boolean.class));
+    verify(l1).isAlreadyCached(key);
     verify(l2).getBlock(key, true, false, true);
   }
 
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
index 8869d30cb55..1d81c0e4462 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
@@ -22,9 +22,11 @@ import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.util.Optional;
 import org.apache.hadoop.hbase.io.hfile.BlockCache;
+import org.apache.hadoop.hbase.io.hfile.CombinedBlockCache;
 import org.apache.hadoop.hbase.testclassification.IOTests;
 import org.apache.hadoop.hbase.testclassification.SmallTests;
 import org.junit.jupiter.api.Tag;
@@ -74,4 +76,56 @@ public class TestTopologyBackedCacheAccessServices {
     assertThrows(NullPointerException.class, () -> 
TopologyBackedCacheAccessServices
       .fromTieredExclusiveBlockCaches("combined", l1, l2, null));
   }
+
+  @Test
+  void testFromCombinedBlockCacheCreatesExpectedService() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    CombinedBlockCache combinedBlockCache = mock(CombinedBlockCache.class);
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+
+    when(combinedBlockCache.getBlockCaches()).thenReturn(new BlockCache[] { 
l1, l2 });
+
+    TopologyBackedCacheAccessService service =
+      
TopologyBackedCacheAccessServices.fromCombinedBlockCache(combinedBlockCache, 
policy);
+
+    assertEquals("combined", service.getName());
+    assertSame(policy, service.getPolicy());
+    assertTrue(service.getTopology() instanceof TieredExclusiveTopology);
+    assertEquals(CacheTopologyType.TIERED_EXCLUSIVE, 
service.getTopology().getType());
+
+    Optional<CacheEngine> l1Engine = 
service.getTopology().getEngine(CacheTier.L1);
+    Optional<CacheEngine> l2Engine = 
service.getTopology().getEngine(CacheTier.L2);
+
+    assertTrue(l1Engine.isPresent());
+    assertTrue(l2Engine.isPresent());
+    assertTrue(l1Engine.get() instanceof BlockCacheBackedCacheEngine);
+    assertTrue(l2Engine.get() instanceof BlockCacheBackedCacheEngine);
+    assertSame(l1, ((BlockCacheBackedCacheEngine) 
l1Engine.get()).getBlockCache());
+    assertSame(l2, ((BlockCacheBackedCacheEngine) 
l2Engine.get()).getBlockCache());
+  }
+
+  @Test
+  void testFromCombinedBlockCacheRejectsNullCombinedBlockCache() {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    assertThrows(NullPointerException.class,
+      () -> TopologyBackedCacheAccessServices.fromCombinedBlockCache(null, 
policy));
+  }
+
+  @Test
+  void testFromCombinedBlockCacheRejectsNullPolicy() {
+    CombinedBlockCache combinedBlockCache = mock(CombinedBlockCache.class);
+    assertThrows(NullPointerException.class,
+      () -> 
TopologyBackedCacheAccessServices.fromCombinedBlockCache(combinedBlockCache, 
null));
+  }
+
+  @Test
+  void testFromCombinedBlockCacheRejectsUnexpectedTierCount() {
+    BlockCache l1 = mock(BlockCache.class);
+    CombinedBlockCache combinedBlockCache = mock(CombinedBlockCache.class);
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(combinedBlockCache.getBlockCaches()).thenReturn(new BlockCache[] { l1 
});
+    assertThrows(IllegalArgumentException.class,
+      () -> 
TopologyBackedCacheAccessServices.fromCombinedBlockCache(combinedBlockCache, 
policy));
+  }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestDataTieringManager.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestDataTieringManager.java
index 9bfe848d968..4e437d72346 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestDataTieringManager.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestDataTieringManager.java
@@ -773,7 +773,6 @@ public class TestDataTieringManager {
     int numHotBlocks = 0, numColdBlocks = 0;
 
     Waiter.waitFor(defaultConf, 10000, 100, () -> (expectedTotalKeys == 
keys.size()));
-    int iter = 0;
     for (BlockCacheKey key : keys) {
       try {
         if (dataTieringManager.isHotData(key)) {
@@ -805,11 +804,6 @@ public class TestDataTieringManager {
     }
   }
 
-  private void 
testDataTieringMethodWithKeyExpectingException(DataTieringMethodCallerWithKey 
caller,
-    BlockCacheKey key, DataTieringException exception) {
-    testDataTieringMethodWithKey(caller, key, false, exception);
-  }
-
   private void 
testDataTieringMethodWithKeyNoException(DataTieringMethodCallerWithKey caller,
     BlockCacheKey key, boolean expectedResult) {
     testDataTieringMethodWithKey(caller, key, expectedResult, null);

Reply via email to