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 f19928ad847 HBASE-30329 Add topology-backed cache diagnostics (#8575)
f19928ad847 is described below

commit f19928ad84726a45adf84baab1562a3209538888
Author: Vladimir Rodionov <[email protected]>
AuthorDate: Wed Sep 2 14:26:25 2026 -0700

    HBASE-30329 Add topology-backed cache diagnostics (#8575)
    
    Signed-off-by: Tak Lon (Stephen) Wu <[email protected]>
---
 .../apache/hadoop/hbase/io/hfile/CacheConfig.java  |  52 ++-
 .../hbase/io/hfile/FirstLevelBlockCache.java       |  12 +
 .../hbase/io/hfile/LruAdaptiveBlockCache.java      |   5 +
 .../hadoop/hbase/io/hfile/LruBlockCache.java       |   5 +
 .../hadoop/hbase/io/hfile/TinyLfuBlockCache.java   |   5 +
 .../hfile/cache/BlockCacheBackedCacheEngine.java   |   5 +
 .../hbase/io/hfile/cache/CacheAccessServices.java  |  48 ++-
 .../hadoop/hbase/io/hfile/cache/CacheEngine.java   |  13 +
 .../hadoop/hbase/io/hfile/cache/CacheTier.java     |   7 +-
 .../hbase/io/hfile/cache/CacheTopologyType.java    |   2 +-
 .../DefaultHBaseCachePlacementAdmissionPolicy.java |  33 +-
 .../hbase/io/hfile/cache/SingleEngineTopology.java |  93 -----
 .../hbase/io/hfile/cache/SingleTierTopology.java   | 180 +++++++++
 .../cache/TopologyBackedCacheAccessService.java    | 149 +++++--
 .../cache/TopologyBackedCacheAccessServices.java   | 189 ++++++++-
 .../hadoop/hbase/io/hfile/TestCacheConfig.java     |  20 +-
 .../hadoop/hbase/io/hfile/TestCacheOnWrite.java    |  10 +-
 .../hfile/cache/CacheAccessServiceTestFactory.java |  12 +-
 .../TestBlockCacheBackedCacheAccessService.java    | 246 ------------
 .../io/hfile/cache/TestCacheAccessServices.java    |   2 +-
 ...CompatibleTopologyBackedCacheAccessService.java |  45 +++
 ...CompatibleTopologyBackedCacheAccessService.java | 433 +++++++++++++++++++++
 ...SingleTierTopologyBackedCacheAccessService.java | 425 ++++++++++++++++++++
 23 files changed, 1551 insertions(+), 440 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 fa69b77f5ec..9ab39b00c90 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
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.hbase.io.hfile;
 
+import java.util.Objects;
 import java.util.Optional;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
@@ -24,9 +25,12 @@ import org.apache.hadoop.hbase.conf.ConfigurationManager;
 import org.apache.hadoop.hbase.conf.PropagatingConfigurationObserver;
 import org.apache.hadoop.hbase.io.ByteBuffAllocator;
 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.BlockCacheBackedCacheEngine;
 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.CacheEngine;
+import org.apache.hadoop.hbase.io.hfile.cache.CacheTier;
+import org.apache.hadoop.hbase.io.hfile.cache.CacheTopology;
 import org.apache.hadoop.hbase.io.hfile.cache.CacheTopologyType;
 import org.apache.hadoop.hbase.io.hfile.cache.TopologyBackedCacheAccessService;
 import org.apache.yetus.audience.InterfaceAudience;
@@ -221,12 +225,52 @@ public class CacheConfig implements 
PropagatingConfigurationObserver {
     initFromConf(conf, family);
     this.byteBuffAllocator = byteBuffAllocator;
     this.cacheAccessService = service != null ? service : 
CacheAccessServices.disabled();
-    this.blockCache = service instanceof BlockCacheBackedCacheAccessService
-      ? ((BlockCacheBackedCacheAccessService) service).getBlockCache()
-      : null;
+    /*
+     * Preserve the legacy BlockCache reference only when the supplied service 
is a direct
+     * single-tier BlockCache adapter. Multi-tier combined caches are 
represented by
+     * TopologyBackedCacheAccessService and intentionally do not expose a 
single legacy BlockCache
+     * through this field. Callers that need cache behavior or diagnostics 
should use
+     * cacheAccessService-level capabilities instead of relying on this legacy 
field.
+     */
+    this.blockCache = unwrapSingleLegacyBlockCache(this.cacheAccessService);
 
   }
 
+  /**
+   * Extracts the legacy {@link BlockCache} from a cache access service when 
that service is backed
+   * by a single {@link BlockCacheBackedCacheEngine}.
+   * <p>
+   * This method exists only to preserve compatibility with legacy {@link 
CacheConfig} callers that
+   * still use the {@link BlockCache} field. The active cache access path 
remains
+   * {@link CacheAccessService}. Code that needs cache behavior or diagnostics 
should use
+   * {@link CacheAccessService} capabilities instead of depending on the 
legacy block cache field.
+   * </p>
+   * @param cacheAccessService cache access service to inspect
+   * @return wrapped legacy block cache when available; otherwise {@code null}
+   * @throws NullPointerException if {@code cacheAccessService} is {@code null}
+   */
+  private static BlockCache unwrapSingleLegacyBlockCache(CacheAccessService 
cacheAccessService) {
+    Objects.requireNonNull(cacheAccessService, "cacheAccessService must not be 
null");
+
+    if (!(cacheAccessService instanceof TopologyBackedCacheAccessService)) {
+      return null;
+    }
+
+    TopologyBackedCacheAccessService topologyBackedService =
+      (TopologyBackedCacheAccessService) cacheAccessService;
+    CacheTopology topology = topologyBackedService.getTopology();
+
+    if (topology.getType() != CacheTopologyType.SINGLE_TIER) {
+      return null;
+    }
+
+    Optional<CacheEngine> engine = topology.getEngine(CacheTier.SINGLE);
+    if (!engine.isPresent() || !(engine.get() instanceof 
BlockCacheBackedCacheEngine)) {
+      return null;
+    }
+    return ((BlockCacheBackedCacheEngine) engine.get()).getBlockCache();
+  }
+
   /**
    * Create a cache configuration using the specified configuration object and 
family descriptor.
    * @param conf   hbase configuration
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/FirstLevelBlockCache.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/FirstLevelBlockCache.java
index 34d6c8d926b..0c96adaeff9 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/FirstLevelBlockCache.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/FirstLevelBlockCache.java
@@ -40,4 +40,16 @@ public interface FirstLevelBlockCache extends 
ResizableBlockCache, HeapSize {
    * @throws IllegalArgumentException if the victim cache had already been set
    */
   void setVictimCache(BlockCache victimCache);
+
+  /**
+   * Removes the configured victim cache from this first-level cache.
+   * <p>
+   * This method is used during the block cache migration when an existing 
first-level cache is
+   * moved under topology-backed orchestration. Legacy combined-cache 
construction wires L1 to L2
+   * through a victim cache. Once L1 and L2 are represented as independent 
topology engines, the
+   * topology must control L2 lookup and promotion directly, so the old 
victim-cache delegation must
+   * be disabled.
+   * </p>
+   */
+  void unsetVictimCache();
 }
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruAdaptiveBlockCache.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruAdaptiveBlockCache.java
index 87932074bff..f5048bb69be 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruAdaptiveBlockCache.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruAdaptiveBlockCache.java
@@ -437,6 +437,11 @@ public class LruAdaptiveBlockCache implements 
FirstLevelBlockCache {
     victimHandler = requireNonNull(victimCache);
   }
 
+  @Override
+  public void unsetVictimCache() {
+    this.victimHandler = null;
+  }
+
   @Override
   public void setMaxSize(long maxSize) {
     this.maxSize = maxSize;
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruBlockCache.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruBlockCache.java
index 1e6205c58e5..dcad4a1d020 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruBlockCache.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/LruBlockCache.java
@@ -346,6 +346,11 @@ public class LruBlockCache implements FirstLevelBlockCache 
{
     victimHandler = requireNonNull(victimCache);
   }
 
+  @Override
+  public void unsetVictimCache() {
+    this.victimHandler = null;
+  }
+
   @Override
   public void setMaxSize(long maxSize) {
     this.maxSize = maxSize;
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/TinyLfuBlockCache.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/TinyLfuBlockCache.java
index 7852f19bd63..d8901e83328 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/TinyLfuBlockCache.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/TinyLfuBlockCache.java
@@ -417,4 +417,9 @@ public final class TinyLfuBlockCache implements 
FirstLevelBlockCache {
   public long getDataBlockCount() {
     return getBlockCount();
   }
+
+  @Override
+  public void unsetVictimCache() {
+    this.victimCache = 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 840fd9380c0..c4f7f9ce611 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
@@ -172,6 +172,11 @@ public class BlockCacheBackedCacheEngine implements 
CacheEngine {
     return blockCache.getCurrentDataSize();
   }
 
+  @Override
+  public long getCurrentSize() {
+    return blockCache.getCurrentSize();
+  }
+
   @Override
   public long getBlockCount() {
     return blockCache.getBlockCount();
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 a020323737a..344cfc19b50 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
@@ -24,6 +24,7 @@ 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.hadoop.hbase.io.hfile.InclusiveCombinedBlockCache;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -47,26 +48,37 @@ public final class CacheAccessServices {
   }
 
   /**
-   * Creates a cache access service backed by an existing block cache.
+   * Creates a {@link CacheAccessService} for the supplied legacy {@link 
BlockCache}.
    * <p>
-   * 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.
+   * All legacy block caches are adapted through {@link 
TopologyBackedCacheAccessService}. Plain
+   * single-tier block caches are represented by {@link SingleTierTopology}. 
Exclusive combined
+   * caches are represented by {@link TieredExclusiveTopology}. Inclusive 
combined caches are
+   * represented by {@link TieredInclusiveTopology}.
    * </p>
-   * @param blockCache block cache to expose through {@link CacheAccessService}
-   * @return cache access service
+   * <p>
+   * {@link InclusiveCombinedBlockCache} is checked before {@link 
CombinedBlockCache} because the
+   * inclusive variant has different residency, promotion, and eviction 
semantics. Routing it
+   * through the exclusive topology would be incorrect.
+   * </p>
+   * @param blockCache legacy block cache to adapt
+   * @return topology-backed cache access service for the supplied block cache
+   * @throws NullPointerException if {@code blockCache} is {@code null}
    */
-
   public static CacheAccessService fromBlockCache(BlockCache blockCache) {
     Objects.requireNonNull(blockCache, "blockCache must not be null");
+
+    if (blockCache instanceof InclusiveCombinedBlockCache) {
+      return TopologyBackedCacheAccessServices
+        .fromInclusiveCombinedBlockCache((InclusiveCombinedBlockCache) 
blockCache);
+    }
+
     if (blockCache instanceof CombinedBlockCache) {
       return TopologyBackedCacheAccessServices
         .fromCombinedBlockCache((CombinedBlockCache) blockCache);
     }
-    return new BlockCacheBackedCacheAccessService(blockCache);
 
+    return TopologyBackedCacheAccessServices.fromSingleBlockCache("single", 
blockCache,
+      DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE);
   }
 
   /**
@@ -80,7 +92,7 @@ public final class CacheAccessServices {
    * The method delegates block-cache construction to
    * {@link BlockCacheFactory#createBlockCache(Configuration)}. If the legacy 
factory creates a
    * {@link BlockCache}, the returned service is backed by that cache through
-   * {@link BlockCacheBackedCacheAccessService}. If the legacy factory does 
not create a cache, this
+   * {@link TopologyBackedCacheAccessService}. If the legacy factory does not 
create a cache, this
    * method returns the disabled/no-op cache access service.
    * </p>
    * <p>
@@ -140,15 +152,6 @@ public final class CacheAccessServices {
    * @return optional iterable cached-block view
    * @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);
-  // }
-  // return Optional.empty();
-  // }
 
   public static Optional<Iterable<CachedBlock>> 
asCachedBlockIterable(CacheAccessService service) {
     Objects.requireNonNull(service, "service must not be null");
@@ -156,11 +159,6 @@ public final class CacheAccessServices {
     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 270cbc24e1b..04020725b57 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
@@ -215,6 +215,19 @@ public interface CacheEngine {
    */
   long getCurrentDataSize();
 
+  /**
+   * Returns the current total size of this cache engine.
+   * <p>
+   * The current size may include cache metadata, allocator overhead, index 
structures, or other
+   * implementation-specific memory that is not part of the cached block 
payload. This value is
+   * intentionally distinct from {@link #getCurrentDataSize()}, which reports 
only cached data size.
+   * </p>
+   * @return current total cache size in bytes
+   */
+  default long getCurrentSize() {
+    return getCurrentDataSize();
+  }
+
   /**
    * Returns the total number of cached blocks.
    * @return total block count
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTier.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTier.java
index 69f1d51a49a..c473bb29482 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTier.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTier.java
@@ -30,7 +30,12 @@ import org.apache.yetus.audience.InterfaceAudience;
 public enum CacheTier {
 
   /**
-   * Single-tier topology engine.
+   * Single cache tier.
+   * <p>
+   * This tier is used by {@link SingleTierTopology}. It represents a topology 
with one active cache
+   * engine without assigning L1 or L2 semantics to that engine. A single-tier 
cache may be backed
+   * by an in-memory cache, a bucket cache, or another cache engine.
+   * </p>
    */
   SINGLE,
 
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTopologyType.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTopologyType.java
index 50ff46d5bde..6a8cac5032e 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTopologyType.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheTopologyType.java
@@ -28,7 +28,7 @@ public enum CacheTopologyType {
   /**
    * A topology with a single cache engine.
    */
-  SINGLE,
+  SINGLE_TIER,
 
   /**
    * A tiered topology where a block normally resides in only one tier at a 
time.
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/DefaultHBaseCachePlacementAdmissionPolicy.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/DefaultHBaseCachePlacementAdmissionPolicy.java
index 039c620a33d..f9137f2e642 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/DefaultHBaseCachePlacementAdmissionPolicy.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/DefaultHBaseCachePlacementAdmissionPolicy.java
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.hbase.io.hfile.cache;
 
+import java.util.Arrays;
 import java.util.Objects;
 import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
 import org.apache.hadoop.hbase.io.hfile.BlockType;
@@ -36,6 +37,9 @@ import org.apache.yetus.audience.InterfaceAudience;
 @InterfaceAudience.Private
 public class DefaultHBaseCachePlacementAdmissionPolicy implements 
CachePlacementAdmissionPolicy {
 
+  public static final DefaultHBaseCachePlacementAdmissionPolicy INSTANCE =
+    new DefaultHBaseCachePlacementAdmissionPolicy();
+
   @Override
   public AdmissionDecision shouldAdmit(BlockCacheKey cacheKey, Cacheable block,
     CacheWriteContext context, AdmissionPriority priority, CacheTopologyView 
topologyView) {
@@ -48,6 +52,24 @@ public class DefaultHBaseCachePlacementAdmissionPolicy 
implements CachePlacement
     return AdmissionDecision.admit();
   }
 
+  /**
+   * Selects the cache tier or tiers for an admitted block.
+   * <p>
+   * Single-tier topology has only one active cache engine and therefore 
selects
+   * {@link CacheTier#SINGLE}. Tiered inclusive topology writes to both L1 and 
L2 because inclusive
+   * residency allows the same block to be present in multiple tiers. Tiered 
exclusive topology
+   * keeps the legacy combined-cache placement behavior where data blocks and 
non-data blocks may be
+   * placed in different tiers.
+   * </p>
+   * @param cacheKey     cache key identifying the block
+   * @param block        block being cached
+   * @param context      cache write context
+   * @param topologyView topology view available to the placement policy
+   * @return tier decision for the admitted block
+   * @throws NullPointerException if {@code cacheKey}, {@code block}, {@code 
context}, or
+   *                              {@code topologyView} is {@code null}
+   */
+
   @Override
   public TierDecision selectTier(BlockCacheKey cacheKey, Cacheable block, 
CacheWriteContext context,
     CacheTopologyView topologyView) {
@@ -55,15 +77,14 @@ public class DefaultHBaseCachePlacementAdmissionPolicy 
implements CachePlacement
     Objects.requireNonNull(block, "block must not be null");
     Objects.requireNonNull(context, "context must not be null");
     Objects.requireNonNull(topologyView, "topologyView must not be null");
-    /*
-     * Default compatibility placement prefers metadata/index/bloom blocks in 
L1 and data blocks in
-     * L2 when both tiers are available, but falls back to any available tier 
rather than rejecting
-     * placement.
-     */
-    if (topologyView.getType() == CacheTopologyType.SINGLE) {
+    if (topologyView.getType() == CacheTopologyType.SINGLE_TIER) {
       return TierDecision.single(CacheTier.SINGLE);
     }
 
+    if (topologyView.getType() == CacheTopologyType.TIERED_INCLUSIVE) {
+      return TierDecision.multiple(Arrays.asList(CacheTier.L1, CacheTier.L2));
+    }
+
     if (isMetaOrIndexBlock(block)) {
       if (topologyView.getEngine(CacheTier.L1).isPresent()) {
         return TierDecision.single(CacheTier.L1);
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/SingleEngineTopology.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/SingleEngineTopology.java
deleted file mode 100644
index c94deabdae5..00000000000
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/SingleEngineTopology.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.Collections;
-import java.util.List;
-import java.util.Optional;
-import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
-import org.apache.hadoop.hbase.io.hfile.CacheStats;
-import org.apache.hadoop.hbase.io.hfile.Cacheable;
-import org.apache.yetus.audience.InterfaceAudience;
-
-/**
- * Single-engine cache topology.
- * <p>
- * This topology wraps a single {@link CacheEngine}. It is primarily useful as 
a baseline topology
- * and as a simple bridge for cache configurations that do not use L1/L2 
tiering.
- * </p>
- */
[email protected]
-public class SingleEngineTopology implements CacheTopology {
-
-  private final String name;
-  private final CacheEngine engine;
-  private final CacheTopologyView view;
-
-  public SingleEngineTopology(String name, CacheEngine engine) {
-    this.name = name;
-    this.engine = engine;
-    this.view = new CacheTopologyView(this);
-  }
-
-  @Override
-  public String getName() {
-    return name;
-  }
-
-  @Override
-  public CacheTopologyType getType() {
-    return CacheTopologyType.SINGLE;
-  }
-
-  @Override
-  public List<CacheEngine> getEngines() {
-    return Collections.singletonList(engine);
-  }
-
-  @Override
-  public Optional<CacheEngine> getEngine(CacheTier tier) {
-    return tier == CacheTier.SINGLE ? Optional.of(engine) : Optional.empty();
-  }
-
-  @Override
-  public CacheTopologyView getView() {
-    return view;
-  }
-
-  @Override
-  public CacheStats getStats() {
-    return engine.getStats();
-  }
-
-  @Override
-  public boolean promote(BlockCacheKey cacheKey, Cacheable block, CacheEngine 
sourceEngine,
-    CacheEngine targetEngine) {
-    return false;
-  }
-
-  @Override
-  public void shutdown() {
-    engine.shutdown();
-  }
-
-  @Override
-  public List<CacheTier> getTiers() {
-    return Collections.singletonList(CacheTier.SINGLE);
-  }
-}
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/SingleTierTopology.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/SingleTierTopology.java
new file mode 100644
index 00000000000..9dfec550767
--- /dev/null
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/SingleTierTopology.java
@@ -0,0 +1,180 @@
+/*
+ * 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.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
+import org.apache.hadoop.hbase.io.hfile.CacheStats;
+import org.apache.hadoop.hbase.io.hfile.Cacheable;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Single-tier cache topology.
+ * <p>
+ * A single-tier topology contains exactly one cache engine. It is used to 
represent legacy
+ * single-tier {@code BlockCache} implementations inside the topology-backed 
cache access framework.
+ * Unlike tiered topologies, this topology does not perform tier 
orchestration, promotion, or
+ * demotion. All cache operations are directed to the single L1 engine.
+ * </p>
+ * <p>
+ * This topology allows plain block caches to use the same {@link 
TopologyBackedCacheAccessService}
+ * path as combined caches while preserving the existing cache implementation 
underneath.
+ * </p>
+ */
[email protected]
+public class SingleTierTopology implements CacheTopology {
+
+  private final String name;
+  private final CacheEngine engine;
+  private final CacheTopologyView view;
+
+  /**
+   * Creates a single-tier cache topology.
+   * @param name   topology name used for diagnostics
+   * @param engine cache engine backing the single tier
+   */
+  public SingleTierTopology(String name, CacheEngine engine) {
+    this.name = name;
+    this.engine = engine;
+    this.view = new CacheTopologyView(this);
+  }
+
+  /**
+   * Returns the diagnostic name of this topology.
+   * @return topology name
+   */
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Returns the topology type.
+   * @return {@link CacheTopologyType#SINGLE_TIER}
+   */
+  @Override
+  public CacheTopologyType getType() {
+    return CacheTopologyType.SINGLE_TIER;
+  }
+
+  /**
+   * Returns the cache engines that participate in this topology.
+   * @return singleton list containing the single cache engine
+   */
+  @Override
+  public List<CacheEngine> getEngines() {
+    return Collections.singletonList(engine);
+  }
+
+  /**
+   * Returns the tiers available in this topology.
+   * <p>
+   * A single-tier topology exposes its only engine through {@link 
CacheTier#SINGLE}. This avoids
+   * assigning L1 or L2 semantics to a cache configuration that has only one 
active engine.
+   * </p>
+   * @return singleton list containing {@link CacheTier#SINGLE}
+   */
+  @Override
+  public List<CacheTier> getTiers() {
+    return Collections.singletonList(CacheTier.SINGLE);
+  }
+
+  /**
+   * Returns the cache engine associated with the requested tier.
+   * <p>
+   * The only valid tier for this topology is {@link CacheTier#SINGLE}. The 
single tier does not
+   * imply L1 or L2 behavior; it only means that the topology has one active 
cache engine.
+   * </p>
+   * @param tier cache tier to resolve
+   * @return the single cache engine for {@link CacheTier#SINGLE}; otherwise
+   *         {@link Optional#empty()}
+   */
+  @Override
+  public Optional<CacheEngine> getEngine(CacheTier tier) {
+    switch (tier) {
+      case SINGLE:
+        return Optional.of(engine);
+      default:
+        return Optional.empty();
+    }
+  }
+
+  /**
+   * Returns the topology view associated with this topology.
+   * @return topology view
+   */
+  @Override
+  public CacheTopologyView getView() {
+    return view;
+  }
+
+  /**
+   * Returns cache statistics for the single cache engine.
+   * @return cache statistics exposed by the backing engine
+   */
+  @Override
+  public CacheStats getStats() {
+    return engine.getStats();
+  }
+
+  /**
+   * Attempts to promote a block within this topology.
+   * <p>
+   * Single-tier topology has no higher or lower tier, so promotion is not 
supported. The method
+   * returns {@code false} without modifying the cache.
+   * </p>
+   * @param cacheKey     cache key identifying the block
+   * @param block        cached block
+   * @param sourceEngine source cache engine
+   * @param targetEngine target cache engine
+   * @return {@code false} because single-tier topology does not support 
promotion
+   */
+  @Override
+  public boolean promote(BlockCacheKey cacheKey, Cacheable block, CacheEngine 
sourceEngine,
+    CacheEngine targetEngine) {
+    return false;
+  }
+
+  /**
+   * Attempts to demote a block within this topology.
+   * <p>
+   * Single-tier topology has no lower tier, so demotion is not supported. The 
method returns
+   * {@code false} without modifying the cache.
+   * </p>
+   * @param cacheKey     cache key identifying the block
+   * @param block        cached block
+   * @param sourceEngine source cache engine
+   * @param targetEngine target cache engine
+   * @return {@code false} because single-tier topology does not support 
demotion
+   */
+  @Override
+  public boolean demote(BlockCacheKey cacheKey, Cacheable block, CacheEngine 
sourceEngine,
+    CacheEngine targetEngine) {
+    return false;
+  }
+
+  /**
+   * Shuts down the single cache engine.
+   */
+  @Override
+  public void shutdown() {
+    engine.shutdown();
+  }
+}
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 828368274e4..f98ddc6f262 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
@@ -18,12 +18,14 @@
 package org.apache.hadoop.hbase.io.hfile.cache;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 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;
@@ -48,19 +50,13 @@ import org.apache.yetus.audience.InterfaceAudience;
  * operations.
  * </p>
  * <p>
- * This class is the topology-backed counterpart to {@link 
BlockCacheBackedCacheAccessService}. The
- * block-cache-backed implementation is useful for incremental migration with 
no behavior change.
- * This implementation is useful once callers are ready to exercise the new 
topology and engine
- * abstractions directly through {@link CacheAccessService}.
- * </p>
- * <p>
  * Representation selection is intentionally not invoked by this initial 
implementation. Until the
  * service can actually apply representation decisions safely, especially 
around HFileBlock
  * lifecycle and packed/unpacked storage, representation policy is left to a 
later integration step.
  * </p>
  */
 @InterfaceAudience.Private
-public class TopologyBackedCacheAccessService implements CacheAccessService {
+public class TopologyBackedCacheAccessService implements CacheAccessService, 
Iterable<CachedBlock> {
 
   private final CacheTopology topology;
   private final CachePlacementAdmissionPolicy policy;
@@ -130,7 +126,6 @@ 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");
@@ -214,23 +209,79 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
   }
 
   /**
-   * Adds a block to the cache using policy-selected target tiers.
+   * Caches a block using the topology-backed cache access service.
    * <p>
-   * This method first asks the configured policy whether the block should be 
admitted. If admitted,
-   * the policy selects the target tier or tiers. The block is then inserted 
into each selected
-   * engine using {@link CacheEngine#cacheBlock(BlockCacheKey, Cacheable, 
boolean, boolean)}.
+   * Single-tier topology preserves the legacy direct block-cache behavior by 
writing admitted
+   * blocks to the only backing engine. Tiered topologies use the placement 
policy to select one or
+   * more target tiers.
    * </p>
+   * @param cacheKey cache key identifying the block
+   * @param block    block to cache
+   * @param context  cache write context
+   * @throws NullPointerException if {@code cacheKey}, {@code block}, or 
{@code context} is
+   *                              {@code null}
+   */
+  @Override
+  public void cacheBlock(BlockCacheKey cacheKey, Cacheable block, 
CacheWriteContext context) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(block, "block must not be null");
+    Objects.requireNonNull(context, "context must not be null");
+
+    if (topology.getType() == CacheTopologyType.SINGLE_TIER) {
+      cacheBlockToSingleTier(cacheKey, block, context);
+      return;
+    }
+
+    cacheBlockToSelectedTiers(cacheKey, block, context);
+  }
+
+  /**
+   * Caches a block into the only engine in a single-tier topology.
    * <p>
-   * The policy's representation decision is intentionally not applied in this 
initial
-   * implementation. The current block object is passed through unchanged.
+   * This method preserves the behavior of the legacy {@link 
BlockCacheBackedCacheAccessService}
+   * path. A single-tier topology has no placement decision to make: admitted 
blocks are written to
+   * the only available engine, which is exposed as {@link CacheTier#SINGLE}.
    * </p>
-   * @param cacheKey block cache key
-   * @param block    block contents
+   * @param cacheKey cache key identifying the block
+   * @param block    block to cache
    * @param context  cache write context
+   * @throws NullPointerException if {@code cacheKey}, {@code block}, or 
{@code context} is
+   *                              {@code null}
    */
+  private void cacheBlockToSingleTier(BlockCacheKey cacheKey, Cacheable block,
+    CacheWriteContext context) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(block, "block must not be null");
+    Objects.requireNonNull(context, "context must not be null");
 
-  @Override
-  public void cacheBlock(BlockCacheKey cacheKey, Cacheable block, 
CacheWriteContext context) {
+    AdmissionDecision admission =
+      policy.shouldAdmit(cacheKey, block, context, AdmissionPriority.NORMAL, 
topologyView);
+    if (!admission.isAdmitted()) {
+      return;
+    }
+
+    Optional<CacheEngine> engine = topology.getEngine(CacheTier.SINGLE);
+    if (!engine.isPresent()) {
+      return;
+    }
+
+    engine.get().cacheBlock(cacheKey, block, context.isInMemory(), 
context.isWaitWhenCache());
+  }
+
+  /**
+   * Caches a block into the tiers selected by the placement policy.
+   * <p>
+   * This method is used for tiered topologies where the placement policy 
decides whether the block
+   * belongs in L1, L2, or multiple tiers.
+   * </p>
+   * @param cacheKey cache key identifying the block
+   * @param block    block to cache
+   * @param context  cache write context
+   * @throws NullPointerException if {@code cacheKey}, {@code block}, or 
{@code context} is
+   *                              {@code null}
+   */
+  private void cacheBlockToSelectedTiers(BlockCacheKey cacheKey, Cacheable 
block,
+    CacheWriteContext context) {
     Objects.requireNonNull(cacheKey, "cacheKey must not be null");
     Objects.requireNonNull(block, "block must not be null");
     Objects.requireNonNull(context, "context must not be null");
@@ -386,12 +437,23 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
   }
 
   /**
-   * Returns aggregate occupied size of the block cache, in bytes.
-   * @return occupied space in cache, in bytes
+   * Returns the current total size of all cache engines in this topology.
+   * <p>
+   * This method aggregates {@link CacheEngine#getCurrentSize()} rather than
+   * {@link CacheEngine#getCurrentDataSize()} so topology-backed diagnostics 
preserve legacy
+   * {@link BlockCache#getCurrentSize()} semantics. For cache implementations 
such as
+   * {@code LruBlockCache}, current size may include metadata and cache 
overhead in addition to
+   * cached data bytes.
+   * </p>
+   * @return aggregate current total cache size in bytes
    */
   @Override
   public long getCurrentSize() {
-    return getCurrentDataSize();
+    long currentSize = 0L;
+    for (CacheEngine engine : topology.getEngines()) {
+      currentSize += engine.getCurrentSize();
+    }
+    return currentSize;
   }
 
   /**
@@ -626,6 +688,22 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
     }
   }
 
+  /**
+   * Returns an iterable view over cached blocks exposed by the cache engines 
in this topology.
+   * <p>
+   * The returned iterable aggregates cached-block iterables from all engines 
that support this
+   * diagnostic capability. Engines that do not expose cached-block iteration 
are skipped. If no
+   * engine supports cached-block iteration, this method returns {@link 
Optional#empty()}.
+   * </p>
+   * <p>
+   * The aggregate iterable uses each underlying iterable's {@link 
Iterable#iterator()} method
+   * instead of {@link Iterable#spliterator()}. This is intentional because 
some legacy cache
+   * implementations and Mockito-based test doubles expose iteration through 
{@code iterator()} but
+   * may not provide a usable {@code spliterator()}.
+   * </p>
+   * @return an aggregated cached-block iterable when at least one engine 
supports this capability;
+   *         otherwise {@link Optional#empty()}
+   */
   public Optional<Iterable<CachedBlock>> asCachedBlockIterable() {
     List<Iterable<CachedBlock>> iterables = new ArrayList<>();
 
@@ -637,9 +715,32 @@ public class TopologyBackedCacheAccessService implements 
CacheAccessService {
       return Optional.empty();
     }
 
-    Iterable<CachedBlock> cachedBlocks = () -> iterables.stream()
-      .flatMap(iterable -> StreamSupport.stream(iterable.spliterator(), 
false)).iterator();
+    Iterable<CachedBlock> cachedBlocks = () -> new Iterator<CachedBlock>() {
+      private final Iterator<Iterable<CachedBlock>> iterableIterator = 
iterables.iterator();
+      private Iterator<CachedBlock> currentIterator = 
Collections.emptyIterator();
+
+      @Override
+      public boolean hasNext() {
+        while (!currentIterator.hasNext() && iterableIterator.hasNext()) {
+          currentIterator = iterableIterator.next().iterator();
+        }
+        return currentIterator.hasNext();
+      }
+
+      @Override
+      public CachedBlock next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        return currentIterator.next();
+      }
+    };
 
     return Optional.of(cachedBlocks);
   }
+
+  @Override
+  public Iterator<CachedBlock> iterator() {
+    return asCachedBlockIterable().orElse(Collections.emptyList()).iterator();
+  }
 }
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 3ce586c22a7..22810f52660 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
@@ -21,6 +21,7 @@ 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.hadoop.hbase.io.hfile.InclusiveCombinedBlockCache;
 import org.apache.yetus.audience.InterfaceAudience;
 
 /**
@@ -103,31 +104,189 @@ 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);
+    if (l1 instanceof FirstLevelBlockCache) {
+      ((FirstLevelBlockCache) l1).unsetVictimCache();
+    }
+    CacheEngine l1Engine = CacheEngines.fromBlockCache(l1);// 
fromL1BlockCache(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.
+   * Creates a topology-backed cache access service for an {@link 
InclusiveCombinedBlockCache}.
+   * <p>
+   * The inclusive combined cache must expose exactly two non-null legacy 
block caches. The first
+   * cache is adapted as L1 using a non-victim-delegating engine, and the 
second cache is adapted as
+   * L2. This prevents L1 misses from internally consulting L2 through the 
legacy victim-cache
+   * mechanism and lets the topology-backed service control tier lookup and 
promotion policy.
+   * </p>
+   * @param combinedBlockCache inclusive combined block cache to adapt
+   * @return topology-backed cache access service using a tiered inclusive 
topology
+   * @throws NullPointerException     if {@code combinedBlockCache} is {@code 
null}
+   * @throws IllegalArgumentException if the combined cache does not expose 
exactly two non-null
+   *                                  block caches
+   */
+  public static TopologyBackedCacheAccessService
+    fromInclusiveCombinedBlockCache(InclusiveCombinedBlockCache 
combinedBlockCache) {
+    Objects.requireNonNull(combinedBlockCache, "combinedBlockCache must not be 
null");
+
+    BlockCache[] blockCaches = combinedBlockCache.getBlockCaches();
+    if (blockCaches == null || blockCaches.length != 2) {
+      throw new IllegalArgumentException(
+        "InclusiveCombinedBlockCache must expose exactly two block caches");
+    }
+    if (blockCaches[0] == null || blockCaches[1] == null) {
+      throw new IllegalArgumentException(
+        "InclusiveCombinedBlockCache must expose non-null L1 and L2 block 
caches");
+    }
+
+    return fromTieredInclusiveBlockCaches("inclusive-combined", 
blockCaches[0], blockCaches[1],
+      DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE);
+  }
+
+  /**
+   * Creates a topology-backed cache access service from two legacy block 
caches using an inclusive
+   * tiered topology.
    * <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.
+   * The first supplied block cache is treated as the L1 tier and the second 
supplied block cache is
+   * treated as the L2 tier. Both legacy caches are adapted to {@link 
CacheEngine} instances using
+   * {@link CacheEngines#fromBlockCache(BlockCache)} and then assembled into a
+   * {@link TieredInclusiveTopology}.
    * </p>
-   * @param l1 first-level block cache
-   * @param l2 second-level block cache
+   * <p>
+   * This helper is intended for compatibility with legacy inclusive 
combined-cache configurations
+   * while moving cache access and diagnostics to the {@link 
CacheAccessService} abstraction.
+   * Inclusive topology semantics differ from exclusive topology semantics: a 
block may exist in
+   * both tiers, and eviction from one tier does not necessarily imply 
eviction from the other tier.
+   * </p>
+   * @param name   topology name used for diagnostics
+   * @param l1     first-level block cache
+   * @param l2     second-level block cache
+   * @param policy cache placement and admission policy to use with the 
topology-backed service
+   * @return topology-backed cache access service backed by a tiered inclusive 
topology
+   * @throws NullPointerException if {@code name}, {@code l1}, {@code l2}, or 
{@code policy} is
+   *                              {@code null}
    */
-  private static void wireVictimCache(BlockCache l1, BlockCache l2) {
+  public static TopologyBackedCacheAccessService 
fromTieredInclusiveBlockCaches(String name,
+    BlockCache l1, BlockCache l2, CachePlacementAdmissionPolicy policy) {
+    Objects.requireNonNull(name, "name must not be null");
+    Objects.requireNonNull(l1, "l1 must not be null");
+    Objects.requireNonNull(l2, "l2 must not be null");
+    Objects.requireNonNull(policy, "policy must not be null");
     if (l1 instanceof FirstLevelBlockCache) {
-      try {
-        ((FirstLevelBlockCache) l1).setVictimCache(l2);
-      } catch (IllegalArgumentException e) {
-        // ignore if already wired
-      }
+      ((FirstLevelBlockCache) l1).unsetVictimCache();
+    }
+    CacheEngine l1Engine = CacheEngines.fromBlockCache(l1);
+    CacheEngine l2Engine = CacheEngines.fromBlockCache(l2);
+    CacheTopology topology = new TieredInclusiveTopology(name, l1Engine, 
l2Engine);
+
+    return new TopologyBackedCacheAccessService(topology, policy);
+  }
+
+  /**
+   * Creates a topology-backed cache access service for a single legacy {@link 
BlockCache}.
+   * <p>
+   * The supplied block cache is adapted to a {@link CacheEngine} and placed 
behind a
+   * {@link SingleTierTopology}. This makes single-tier caches use the same
+   * {@link TopologyBackedCacheAccessService} path as combined caches while 
preserving the existing
+   * block cache implementation underneath.
+   * </p>
+   * @param name       topology name used for diagnostics
+   * @param blockCache legacy block cache to adapt
+   * @param policy     cache placement and admission policy
+   * @return topology-backed cache access service backed by a single-tier 
topology
+   * @throws NullPointerException if {@code name}, {@code blockCache}, or 
{@code policy} is
+   *                              {@code null}
+   */
+  public static TopologyBackedCacheAccessService fromSingleBlockCache(String 
name,
+    BlockCache blockCache, CachePlacementAdmissionPolicy policy) {
+    Objects.requireNonNull(name, "name must not be null");
+    Objects.requireNonNull(blockCache, "blockCache must not be null");
+    Objects.requireNonNull(policy, "policy must not be null");
+
+    CacheEngine engine = CacheEngines.fromBlockCache(blockCache);
+    CacheTopology topology = new SingleTierTopology(name, engine);
+    return new TopologyBackedCacheAccessService(topology, policy);
+  }
+
+  /**
+   * Returns the legacy {@link BlockCache} wrapped by the cache engine for the 
requested tier.
+   * <p>
+   * This helper is intended for tests that need to verify compatibility with 
legacy block cache
+   * implementations during the migration to topology-backed cache access. 
Production code should
+   * prefer {@link CacheAccessService} capability methods instead of 
unwrapping the underlying
+   * {@link BlockCache}.
+   * </p>
+   * <p>
+   * The supplied service must be a {@link TopologyBackedCacheAccessService}. 
The requested tier
+   * must resolve to a {@link BlockCacheBackedCacheEngine}. If either 
condition is not true, this
+   * method fails fast with an {@link IllegalArgumentException}.
+   * </p>
+   * @param cacheAccessService cache access service to inspect
+   * @param tier               cache tier to unwrap
+   * @return legacy block cache wrapped by the cache engine for the requested 
tier
+   * @throws NullPointerException     if {@code cacheAccessService} or {@code 
tier} is {@code null}
+   * @throws IllegalArgumentException if the service is not topology-backed, 
if the requested tier
+   *                                  is not present, or if the tier is not 
backed by a
+   *                                  {@link BlockCacheBackedCacheEngine}
+   */
+  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;
+    CacheTopology topology = topologyBackedService.getTopology();
+
+    CacheEngine engine = topology.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();
+  }
+
+  /**
+   * Returns the legacy {@link BlockCache} wrapped by a single-tier 
topology-backed cache access
+   * service.
+   * <p>
+   * Single-tier topology exposes its only engine through {@link 
CacheTier#SINGLE}. The only active
+   * engine is not assumed to be L1 or L2 because a single-tier configuration 
may be backed by
+   * different concrete cache implementations, including bucket cache.
+   * </p>
+   * @param cacheAccessService cache access service to inspect
+   * @return legacy block cache wrapped by the single-tier cache engine
+   * @throws NullPointerException     if {@code cacheAccessService} is {@code 
null}
+   * @throws IllegalArgumentException if the supplied service is not a 
topology-backed single-tier
+   *                                  cache service or if the single tier is 
not backed by a
+   *                                  {@link BlockCacheBackedCacheEngine}
+   */
+  public static BlockCache getBlockCache(CacheAccessService 
cacheAccessService) {
+    Objects.requireNonNull(cacheAccessService, "cacheAccessService must not be 
null");
+
+    if (!(cacheAccessService instanceof TopologyBackedCacheAccessService)) {
+      throw new IllegalArgumentException(
+        "cacheAccessService must be a TopologyBackedCacheAccessService");
     }
+
+    TopologyBackedCacheAccessService topologyBackedService =
+      (TopologyBackedCacheAccessService) cacheAccessService;
+    CacheTopology topology = topologyBackedService.getTopology();
+
+    if (topology.getType() != CacheTopologyType.SINGLE_TIER) {
+      throw new IllegalArgumentException(
+        "cacheAccessService must be backed by a single-tier topology");
+    }
+
+    return getBlockCache(cacheAccessService, CacheTier.SINGLE);
   }
 }
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 1d7286dbe6c..2aa5a8d6991 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
@@ -43,10 +43,11 @@ import 
org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
 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.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.NoOpCacheAccessService;
+import org.apache.hadoop.hbase.io.hfile.cache.TopologyBackedCacheAccessService;
+import 
org.apache.hadoop.hbase.io.hfile.cache.TopologyBackedCacheAccessServices;
 import org.apache.hadoop.hbase.io.util.MemorySizeUtil;
 import org.apache.hadoop.hbase.nio.ByteBuff;
 import org.apache.hadoop.hbase.testclassification.IOTests;
@@ -314,7 +315,6 @@ public class TestCacheConfig {
     // TODO: Assert sizes allocated are right and proportions.
     LruBlockCache lbc =
       (LruBlockCache) 
CacheAccessServiceTestFactory.getFirstLevelBlockCache(service);
-    ;
     assertEquals(MemorySizeUtil.getOnHeapCacheSize(this.conf), 
lbc.getMaxSize());
     BucketCache bc = (BucketCache) 
CacheAccessServiceTestFactory.getSecondLevelBlockCache(service);
     // getMaxSize comes back in bytes but we specified size in MB
@@ -381,19 +381,23 @@ public class TestCacheConfig {
     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.
+    /*
+     * The topology-backed cache path intentionally clears legacy L1 
victim-cache wiring when L1 and
+     * L2 are adapted as independent topology engines. Direct calls to the 
unwrapped L1 cache should
+     * therefore not be used to verify L1-to-L2 victim movement. Tier 
placement, promotion, and
+     * lookup are now owned by TopologyBackedCacheAccessService.
+     */
     long initialL1BlockCount = lbc.getBlockCount();
     long initialL2BlockCount = bc.getBlockCount();
     Cacheable c = new DataCacheEntry();
     BlockCacheKey bck = new BlockCacheKey("bck", 0);
+
     lbc.cacheBlock(bck, c, false);
+
     assertEquals(initialL1BlockCount + 1, lbc.getBlockCount());
     assertEquals(initialL2BlockCount, bc.getBlockCount());
-
     assertNotNull(lbc.getBlock(bck, true, false, true));
     assertNull(bc.getBlock(bck, true, false, true));
-    waitForAnyBlockToMoveFromL1ToL2(lbc, bc, initialL2BlockCount);
-    assertTrue(bc.getBlockCount() > initialL2BlockCount);
   }
 
   /**
@@ -491,8 +495,8 @@ public class TestCacheConfig {
 
     CacheAccessService service = cacheConfig.getCacheAccessService();
 
-    assertInstanceOf(BlockCacheBackedCacheAccessService.class, service);
-    assertSame(blockCache, ((BlockCacheBackedCacheAccessService) 
service).getBlockCache());
+    assertInstanceOf(TopologyBackedCacheAccessService.class, service);
+    assertSame(blockCache, 
TopologyBackedCacheAccessServices.getBlockCache(service));
   }
 
   @Test
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheOnWrite.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheOnWrite.java
index a016cbef03a..75a03d42504 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheOnWrite.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/TestCacheOnWrite.java
@@ -50,10 +50,10 @@ import org.apache.hadoop.hbase.fs.HFileSystem;
 import org.apache.hadoop.hbase.io.compress.Compression;
 import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
 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.io.hfile.cache.TopologyBackedCacheAccessServices;
 import org.apache.hadoop.hbase.regionserver.BloomType;
 import org.apache.hadoop.hbase.regionserver.HRegion;
 import org.apache.hadoop.hbase.regionserver.StoreFileWriter;
@@ -202,7 +202,7 @@ public class TestCacheOnWrite {
 
   private void clearBlockCache(CacheAccessService cache) throws 
InterruptedException {
     // TODO: HBASE-30018 refactor later
-    BlockCache blockCache = ((BlockCacheBackedCacheAccessService) 
cache).getBlockCache();
+    BlockCache blockCache = 
TopologyBackedCacheAccessServices.getBlockCache(cache);
     if (blockCache instanceof LruBlockCache) {
       ((LruBlockCache) blockCache).clearCache();
     } else {
@@ -240,7 +240,7 @@ public class TestCacheOnWrite {
       cowType.shouldBeCached(BlockType.LEAF_INDEX));
     conf.setBoolean(CacheConfig.CACHE_BLOOM_BLOCKS_ON_WRITE_KEY,
       cowType.shouldBeCached(BlockType.BLOOM_CHUNK));
-    cacheConf = new CacheConfig(conf, ((BlockCacheBackedCacheAccessService) 
cache).getBlockCache());
+    cacheConf = new CacheConfig(conf, 
TopologyBackedCacheAccessServices.getBlockCache(cache));
     fs = HFileSystem.get(conf);
   }
 
@@ -442,7 +442,7 @@ public class TestCacheOnWrite {
         
.setCompressionType(compress).setBloomFilterType(BLOOM_TYPE).setMaxVersions(maxVersions)
         
.setDataBlockEncoding(NoOpDataBlockEncoder.INSTANCE.getDataBlockEncoding()).build();
       HRegion region = TEST_UTIL.createTestRegion(table, cfd,
-        ((BlockCacheBackedCacheAccessService) cache).getBlockCache());
+        TopologyBackedCacheAccessServices.getBlockCache(cache));
       int rowIdx = 0;
       long ts = EnvironmentEdgeManager.currentTime();
       for (int iFile = 0; iFile < 5; ++iFile) {
@@ -500,7 +500,7 @@ public class TestCacheOnWrite {
       // of testing
       // BucketCache, we cannot verify block type as it is not stored in the 
cache.
       boolean cacheOnCompactAndNonBucketCache = cacheBlocksOnCompaction
-        && !(((BlockCacheBackedCacheAccessService) cache).getBlockCache() 
instanceof BucketCache);
+        && !(TopologyBackedCacheAccessServices.getBlockCache(cache) instanceof 
BucketCache);
 
       String assertErrorMessage = "\nTest description: " + testDescription
         + "\ncacheBlocksOnCompaction: " + cacheBlocksOnCompaction + "\n";
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 37dbc0a9227..dc31fab4607 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
@@ -649,12 +649,6 @@ public final class CacheAccessServiceTestFactory {
    * direct access to the underlying legacy {@link BlockCache} for 
implementation-specific
    * assertions, cached-block iteration, metrics inspection, or other 
diagnostic checks.
    * </p>
-   * <p>
-   * Only {@link BlockCacheBackedCacheAccessService} is supported. Services 
backed by future
-   * topology/cache-engine implementations are not required to expose a legacy 
{@link BlockCache}.
-   * Tests that use this method should therefore be treated as compatibility 
tests, not as tests of
-   * the final pluggable-cache architecture.
-   * </p>
    * @param cacheAccessService cache access service
    * @return backing legacy block cache
    * @throws NullPointerException     if {@code cacheAccessService} is {@code 
null}
@@ -663,11 +657,7 @@ public final class CacheAccessServiceTestFactory {
    */
   public static BlockCache blockCache(CacheAccessService cacheAccessService) {
     Objects.requireNonNull(cacheAccessService, "cacheAccessService must not be 
null");
-    if (cacheAccessService instanceof BlockCacheBackedCacheAccessService) {
-      return ((BlockCacheBackedCacheAccessService) 
cacheAccessService).getBlockCache();
-    }
-    throw new IllegalArgumentException("CacheAccessService is not backed by a 
legacy BlockCache: "
-      + cacheAccessService.getClass().getName());
+    return TopologyBackedCacheAccessServices.getBlockCache(cacheAccessService);
   }
 
   /**
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestBlockCacheBackedCacheAccessService.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestBlockCacheBackedCacheAccessService.java
deleted file mode 100644
index 8a7378c7b83..00000000000
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestBlockCacheBackedCacheAccessService.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.Assert.assertThrows;
-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.assertNull;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.Optional;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.Path;
-import org.apache.hadoop.hbase.io.hfile.BlockCache;
-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.HFileBlock;
-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 BlockCacheBackedCacheAccessService} and related service 
helpers.
- */
-@Tag(IOTests.TAG)
-@Tag(SmallTests.TAG)
-public class TestBlockCacheBackedCacheAccessService {
-
-  private static final String HFILE_NAME = "file";
-
-  private static final long BLOCK_OFFSET = 1L;
-
-  private static final long RANGE_START_OFFSET = 1L;
-
-  private static final long RANGE_END_OFFSET = 10L;
-
-  /**
-   * Verifies that context-based lookup delegates to the block-type aware 
legacy lookup method.
-   */
-  @Test
-  void testGetBlockWithBlockTypeDelegatesToBlockCache() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = new 
BlockCacheBackedCacheAccessService(blockCache);
-    BlockCacheKey key = new BlockCacheKey(HFILE_NAME, BLOCK_OFFSET);
-    Cacheable block = mock(Cacheable.class);
-
-    when(blockCache.getBlock(key, true, true, false, 
BlockType.DATA)).thenReturn(block);
-
-    CacheRequestContext context = 
CacheRequestContext.newBuilder().withCaching(true)
-      
.withRepeat(true).withUpdateCacheMetrics(false).withBlockType(BlockType.DATA).build();
-
-    assertSame(block, service.getBlock(key, context));
-    verify(blockCache).getBlock(key, true, true, false, BlockType.DATA);
-  }
-
-  /**
-   * Verifies that context-based lookup delegates to the legacy lookup method 
without block type.
-   */
-  @Test
-  void testGetBlockWithoutBlockTypeDelegatesToBlockCache() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = new 
BlockCacheBackedCacheAccessService(blockCache);
-    BlockCacheKey key = new BlockCacheKey(HFILE_NAME, BLOCK_OFFSET);
-    Cacheable block = mock(Cacheable.class);
-
-    when(blockCache.getBlock(key, true, false, true)).thenReturn(block);
-
-    CacheRequestContext context = 
CacheRequestContext.newBuilder().withCaching(true)
-      .withRepeat(false).withUpdateCacheMetrics(true).build();
-
-    assertSame(block, service.getBlock(key, context));
-    verify(blockCache).getBlock(key, true, false, true);
-  }
-
-  /**
-   * Verifies that context-based insertion delegates in-memory and wait flags 
correctly.
-   */
-  @Test
-  void testCacheBlockDelegatesToBlockCache() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = new 
BlockCacheBackedCacheAccessService(blockCache);
-    BlockCacheKey key = new BlockCacheKey(HFILE_NAME, BLOCK_OFFSET);
-    Cacheable block = mock(Cacheable.class);
-
-    CacheWriteContext context = 
CacheWriteContext.newBuilder().withInMemory(true)
-      .withWaitWhenCache(true).withSource(CacheWriteSource.READ_MISS).build();
-
-    service.cacheBlock(key, block, context);
-
-    verify(blockCache).cacheBlock(key, block, true, true);
-  }
-
-  /**
-   * Verifies that invalidation methods delegate to the wrapped block cache.
-   */
-  @Test
-  void testEvictionDelegatesToBlockCache() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = new 
BlockCacheBackedCacheAccessService(blockCache);
-    BlockCacheKey key = new BlockCacheKey(HFILE_NAME, BLOCK_OFFSET);
-
-    when(blockCache.evictBlock(key)).thenReturn(true);
-    when(blockCache.evictBlocksByHfileName(HFILE_NAME)).thenReturn(3);
-    when(blockCache.evictBlocksRangeByHfileName(HFILE_NAME, 
RANGE_START_OFFSET, RANGE_END_OFFSET))
-      .thenReturn(2);
-
-    assertTrue(service.evictBlock(key));
-    assertEquals(3, service.evictBlocksByHfileName(HFILE_NAME));
-    assertEquals(2,
-      service.evictBlocksRangeByHfileName(HFILE_NAME, RANGE_START_OFFSET, 
RANGE_END_OFFSET));
-
-    verify(blockCache).evictBlock(key);
-    verify(blockCache).evictBlocksByHfileName(HFILE_NAME);
-    verify(blockCache).evictBlocksRangeByHfileName(HFILE_NAME, 
RANGE_START_OFFSET,
-      RANGE_END_OFFSET);
-  }
-
-  /**
-   * Verifies that stats, sizing, lifecycle, and optional helpers delegate to 
the wrapped cache.
-   */
-  @Test
-  void testStatsSizingLifecycleAndHelpersDelegateToBlockCache() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = new 
BlockCacheBackedCacheAccessService(blockCache);
-    CacheStats stats = new CacheStats("test");
-    HFileBlock hfileBlock = mock(HFileBlock.class);
-    BlockCacheKey key = new BlockCacheKey(HFILE_NAME, BLOCK_OFFSET);
-    Configuration conf = new Configuration(false);
-
-    when(blockCache.getStats()).thenReturn(stats);
-    when(blockCache.getMaxSize()).thenReturn(100L);
-    when(blockCache.getFreeSize()).thenReturn(40L);
-    when(blockCache.size()).thenReturn(60L);
-    when(blockCache.getCurrentDataSize()).thenReturn(50L);
-    when(blockCache.getBlockCount()).thenReturn(10L);
-    when(blockCache.getDataBlockCount()).thenReturn(8L);
-    
when(blockCache.blockFitsIntoTheCache(hfileBlock)).thenReturn(Optional.of(true));
-    when(blockCache.isAlreadyCached(key)).thenReturn(Optional.of(false));
-    when(blockCache.getBlockSize(key)).thenReturn(Optional.of(123));
-    when(blockCache.isCacheEnabled()).thenReturn(true);
-    when(blockCache.waitForCacheInitialization(500L)).thenReturn(true);
-
-    assertSame(stats, service.getStats());
-    assertEquals(100L, service.getMaxSize());
-    assertEquals(40L, service.getFreeSize());
-    assertEquals(60L, service.size());
-    assertEquals(50L, service.getCurrentDataSize());
-    assertEquals(10L, service.getBlockCount());
-    assertEquals(8L, service.getDataBlockCount());
-    assertEquals(Optional.of(true), service.blockFitsIntoTheCache(hfileBlock));
-    assertEquals(Optional.of(false), service.isAlreadyCached(key));
-    assertEquals(Optional.of(123), service.getBlockSize(key));
-    assertTrue(service.isCacheEnabled());
-    assertTrue(service.waitForCacheInitialization(500L));
-
-    service.onConfigurationChange(conf);
-    service.shutdown();
-
-    verify(blockCache).onConfigurationChange(conf);
-    verify(blockCache).shutdown();
-  }
-
-  /**
-   * Verifies factory helper methods.
-   */
-  @Test
-  void testCacheAccessServicesFactoryMethods() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = 
CacheAccessServices.fromBlockCache(blockCache);
-
-    assertInstanceOf(BlockCacheBackedCacheAccessService.class, service);
-    assertSame(blockCache, ((BlockCacheBackedCacheAccessService) 
service).getBlockCache());
-    assertInstanceOf(NoOpCacheAccessService.class, 
CacheAccessServices.disabled());
-  }
-
-  /**
-   * Verifies disabled-cache behavior.
-   */
-  @Test
-  void testNoOpCacheAccessService() {
-    CacheAccessService service = new NoOpCacheAccessService(new 
CacheStats("noop"));
-
-    assertEquals("NoOpCacheAccessService", service.getName());
-    assertNull(service.getBlock(mock(BlockCacheKey.class), 
mock(CacheRequestContext.class)));
-    service.cacheBlock(mock(BlockCacheKey.class), mock(Cacheable.class),
-      mock(CacheWriteContext.class));
-    assertFalse(service.evictBlock(mock(BlockCacheKey.class)));
-    assertEquals(0, service.evictBlocksByHfileName(HFILE_NAME));
-    assertEquals(0L, service.getMaxSize());
-    assertEquals(0L, service.getFreeSize());
-    assertEquals(0L, service.size());
-    assertEquals(0L, service.getCurrentDataSize());
-    assertEquals(0L, service.getBlockCount());
-    assertEquals(0L, service.getDataBlockCount());
-    assertFalse(service.isCacheEnabled());
-    assertFalse(service.waitForCacheInitialization(1L));
-    service.onConfigurationChange(new Configuration(false));
-    service.shutdown();
-  }
-
-  @Test
-  void testNotifyFileCachingCompletedDelegatesToBlockCache() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = new 
BlockCacheBackedCacheAccessService(blockCache);
-    Path fileName = new Path("/hbase/table/region/family/file");
-    int totalBlockCount = 10;
-    int dataBlockCount = 8;
-    long size = 1024L;
-
-    service.notifyFileCachingCompleted(fileName, totalBlockCount, 
dataBlockCount, size);
-
-    verify(blockCache).notifyFileCachingCompleted(fileName, totalBlockCount, 
dataBlockCount, size);
-  }
-
-  @Test
-  void testNotifyFileCachingCompletedRejectsNullPath() {
-    BlockCache blockCache = mock(BlockCache.class);
-    CacheAccessService service = new 
BlockCacheBackedCacheAccessService(blockCache);
-
-    assertThrows(NullPointerException.class,
-      () -> service.notifyFileCachingCompleted(null, 10, 8, 1024L));
-  }
-}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheAccessServices.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheAccessServices.java
index 1c548b9f930..65dff577bf3 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheAccessServices.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheAccessServices.java
@@ -39,7 +39,7 @@ public class TestCacheAccessServices {
   void testFromBlockCacheCreatesBlockCacheBackedServiceForRegularBlockCache() {
     BlockCache blockCache = mock(BlockCache.class);
     CacheAccessService service = 
CacheAccessServices.fromBlockCache(blockCache);
-    assertTrue(service instanceof BlockCacheBackedCacheAccessService);
+    assertTrue(service instanceof TopologyBackedCacheAccessService);
   }
 
   @Test
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 209ceeae202..c986fc7fa75 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
@@ -34,6 +34,7 @@ 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;
+import org.apache.hadoop.hbase.io.hfile.FirstLevelBlockCache;
 import org.apache.hadoop.hbase.testclassification.IOTests;
 import org.apache.hadoop.hbase.testclassification.SmallTests;
 import org.junit.jupiter.api.Tag;
@@ -207,6 +208,50 @@ public class 
TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
     verify(l2).shutdown();
   }
 
+  /**
+   * Verifies that topology-backed current-size reporting aggregates current 
size across tiers.
+   * <p>
+   * Current size includes implementation-specific overhead and is distinct 
from current data size.
+   * The topology-backed service should therefore aggregate {@link 
CacheEngine#getCurrentSize()}
+   * from all engines rather than using data-size counters.
+   * </p>
+   */
+  @Test
+  void testCurrentSizeAggregatesAllTiers() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+
+    when(l1.getCurrentSize()).thenReturn(100L);
+    when(l2.getCurrentSize()).thenReturn(200L);
+    when(l1.getCurrentDataSize()).thenReturn(10L);
+    when(l2.getCurrentDataSize()).thenReturn(20L);
+
+    TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
+
+    assertEquals(300L, service.getCurrentSize());
+    assertEquals(30L, service.getCurrentDataSize());
+  }
+
+  /**
+   * Verifies that tiered topology construction disables legacy L1 
victim-cache delegation.
+   * <p>
+   * Legacy combined-cache construction wires the first-level cache to the 
second-level cache
+   * through a victim cache. Once the caches are adapted as independent 
topology engines, the
+   * topology-backed service must control L2 lookup directly. The factory 
therefore removes the
+   * legacy victim-cache wiring before adapting L1.
+   * </p>
+   */
+  @Test
+  void testTieredExclusiveFactoryUnsetsL1VictimCache() {
+    FirstLevelBlockCache l1 = mock(FirstLevelBlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+
+    
TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches("combined", 
l1, l2,
+      noPromotionPolicy());
+
+    verify(l1).unsetVictimCache();
+  }
+
   private static TopologyBackedCacheAccessService service(BlockCache l1, 
BlockCache l2,
     CachePlacementAdmissionPolicy policy) {
     return 
TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches("combined", 
l1, l2,
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestInclusiveCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestInclusiveCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
new file mode 100644
index 00000000000..a0a5185128a
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestInclusiveCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
@@ -0,0 +1,433 @@
+/*
+ * 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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+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;
+import org.apache.hadoop.hbase.io.hfile.CachedBlock;
+import org.apache.hadoop.hbase.io.hfile.FirstLevelBlockCache;
+import org.apache.hadoop.hbase.io.hfile.InclusiveCombinedBlockCache;
+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;
+
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class 
TestInclusiveCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
+
+  /**
+   * Verifies that {@link InclusiveCombinedBlockCache} is routed to a 
topology-backed cache access
+   * service using {@link CacheTopologyType#TIERED_INCLUSIVE}.
+   * <p>
+   * This protects against accidentally routing {@link 
InclusiveCombinedBlockCache} through the
+   * exclusive combined-cache topology path. Inclusive and exclusive combined 
caches have different
+   * residency, promotion, and eviction semantics, so they must be represented 
by different topology
+   * types.
+   * </p>
+   */
+  @Test
+  void testInclusiveCombinedBlockCacheUsesTieredInclusiveTopology() {
+    InclusiveCombinedBlockCache combinedBlockCache = 
mock(InclusiveCombinedBlockCache.class);
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+
+    when(combinedBlockCache.getBlockCaches()).thenReturn(new BlockCache[] { 
l1, l2 });
+
+    CacheAccessService service = 
CacheAccessServices.fromBlockCache(combinedBlockCache);
+
+    assertTrue(service instanceof TopologyBackedCacheAccessService);
+    TopologyBackedCacheAccessService topologyBackedService =
+      (TopologyBackedCacheAccessService) service;
+    assertEquals(CacheTopologyType.TIERED_INCLUSIVE, 
topologyBackedService.getTopology().getType());
+  }
+
+  /**
+   * Verifies that inclusive topology lookup checks L1 first and returns the 
L1 block when it is
+   * present.
+   * <p>
+   * Inclusive caches prefer the first-level cache for reads. If a block is 
found in L1, the
+   * topology-backed service should return it without consulting L2.
+   * </p>
+   */
+  @Test
+  void testInclusiveL1HitReturnsBlockWithoutCheckingL2() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    when(l1.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(l2, never()).getBlock(any(), anyBoolean(), anyBoolean(), 
anyBoolean());
+  }
+
+  /**
+   * Verifies that inclusive topology lookup checks L2 after an L1 miss.
+   * <p>
+   * Unlike the exclusive combined-cache compatibility path, inclusive lookup 
does not need the L1
+   * membership shortcut. A normal ordered tier scan is appropriate: L1 is 
checked first, and L2 is
+   * checked only if L1 does not contain the block.
+   * </p>
+   */
+  @Test
+  void testInclusiveL2HitReturnsBlockAfterL1Miss() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    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(l2).getBlock(key, true, false, true);
+  }
+
+  /**
+   * Verifies that promotion in an inclusive topology copies a block to L1 
without evicting it from
+   * L2.
+   * <p>
+   * This is the key semantic difference from the exclusive topology. In an 
exclusive topology,
+   * promotion moves the block from L2 to L1 and removes the L2 copy. In an 
inclusive topology, the
+   * block may remain resident in both tiers.
+   * </p>
+   */
+  @Test
+  void testInclusivePromotionCopiesBlockToL1WithoutEvictingL2() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    when(l1.getBlock(key, true, false, true)).thenReturn(null);
+    when(l2.getBlock(key, true, false, true)).thenReturn(block);
+
+    TopologyBackedCacheAccessService service = service(l1, l2, 
promoteL2HitToL1Policy());
+
+    assertSame(block, service.getBlock(key, requestContext()));
+
+    verify(l1).getBlock(key, true, false, true);
+    verify(l2).getBlock(key, true, false, true);
+    verify(l1).cacheBlock(key, block);
+    verify(l2, never()).evictBlock(key);
+  }
+
+  /**
+   * Verifies that service-level eviction for an inclusive topology evicts 
from all tiers.
+   * <p>
+   * An inclusive cache may contain the same block in both L1 and L2. Evicting 
only the first
+   * matching tier could leave another resident copy behind, so the 
topology-backed service must ask
+   * both tiers to evict the key.
+   * </p>
+   */
+  @Test
+  void testInclusiveEvictBlockEvictsFromBothTiers() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+
+    when(l1.evictBlock(key)).thenReturn(true);
+    when(l2.evictBlock(key)).thenReturn(true);
+
+    TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
+
+    assertTrue(service.evictBlock(key));
+
+    verify(l1).evictBlock(key);
+    verify(l2).evictBlock(key);
+  }
+
+  /**
+   * Verifies that an inclusive topology can cache a block into both tiers 
when the placement policy
+   * selects both L1 and L2.
+   * <p>
+   * This covers the inclusive cache residency model where the same block can 
be intentionally
+   * present in multiple tiers.
+   * </p>
+   */
+  @Test
+  void testInclusiveCacheBlockToBothTiers() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    TopologyBackedCacheAccessService service =
+      service(l1, l2, admitToTiersPolicy(CacheTier.L1, CacheTier.L2));
+
+    service.cacheBlock(key, block, writeContext());
+
+    verify(l1).cacheBlock(key, block, false, false);
+    verify(l2).cacheBlock(key, block, false, false);
+  }
+
+  /**
+   * Verifies that cached-block iteration is aggregated across both tiers for 
inclusive topology.
+   * <p>
+   * Diagnostic code and compatibility tests use
+   * {@link CacheAccessServices#asCachedBlockIterable(CacheAccessService)} to 
enumerate cached
+   * blocks through the active cache access service. Since an inclusive 
topology has multiple
+   * backing engines, the service must expose cached blocks from both L1 and 
L2.
+   * </p>
+   */
+  @Test
+  void testInclusiveCachedBlockIterableAggregatesBothTiers() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    CachedBlock l1Block = mock(CachedBlock.class);
+    CachedBlock l2Block = mock(CachedBlock.class);
+
+    when(l1.iterator()).thenReturn(Arrays.asList(l1Block).iterator());
+    when(l2.iterator()).thenReturn(Arrays.asList(l2Block).iterator());
+
+    TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
+
+    Optional<Iterable<CachedBlock>> iterable = 
CacheAccessServices.asCachedBlockIterable(service);
+
+    assertTrue(iterable.isPresent());
+    assertEquals(Arrays.asList(l1Block, l2Block), toList(iterable.get()));
+  }
+
+  /**
+   * Verifies that shutting down an inclusive topology-backed service shuts 
down both cache tiers.
+   * <p>
+   * The topology-backed service owns the topology-level lifecycle. For a 
two-tier inclusive
+   * topology, shutdown should be delegated to both L1 and L2 engines.
+   * </p>
+   */
+  @Test
+  void testInclusiveShutdownShutsDownBothTiers() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+
+    TopologyBackedCacheAccessService service = service(l1, l2, 
noPromotionPolicy());
+
+    service.shutdown();
+
+    verify(l1).shutdown();
+    verify(l2).shutdown();
+  }
+
+  /**
+   * Verifies that a real {@link InclusiveCombinedBlockCache} is adapted so 
topology-backed lookup
+   * controls L1 and L2 access explicitly.
+   * <p>
+   * The legacy inclusive combined-cache constructor wires the first-level 
cache to the second-level
+   * cache through the victim-cache path. When the cache is adapted to the 
topology-backed model,
+   * that legacy victim wiring must be removed so an L1 miss does not 
internally delegate to L2.
+   * </p>
+   * <p>
+   * After the wiring is removed, the topology-backed service should perform a 
normal inclusive
+   * lookup: query L1 first, observe the miss, then query L2 explicitly.
+   * </p>
+   */
+  @Test
+  void testRealInclusiveCombinedCacheDoesNotDelegateL1MissThroughVictimCache() 
{
+    FirstLevelBlockCache l1 = mock(FirstLevelBlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    InclusiveCombinedBlockCache combinedBlockCache = new 
InclusiveCombinedBlockCache(l1, l2);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    when(l1.getBlock(key, true, false, true)).thenReturn(null);
+    when(l2.getBlock(key, true, false, true)).thenReturn(block);
+
+    CacheAccessService service = 
CacheAccessServices.fromBlockCache(combinedBlockCache);
+
+    assertSame(block, service.getBlock(key, requestContext()));
+
+    verify(l1).unsetVictimCache();
+    verify(l1).getBlock(key, true, false, true);
+    verify(l2).getBlock(key, true, false, true);
+  }
+
+  /**
+   * Verifies that the default placement policy writes inclusive topology 
blocks to both tiers.
+   * <p>
+   * Inclusive topology allows the same block to be resident in L1 and L2, so 
default placement
+   * should not split blocks by data or metadata type the way the exclusive 
combined-cache path
+   * does.
+   * </p>
+   */
+  @Test
+  void testDefaultPolicyWritesInclusiveBlocksToBothTiers() {
+    BlockCache l1 = mock(BlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    TopologyBackedCacheAccessService service =
+      service(l1, l2, DefaultHBaseCachePlacementAdmissionPolicy.INSTANCE);
+
+    service.cacheBlock(key, block, writeContext());
+
+    verify(l1).cacheBlock(key, block, false, false);
+    verify(l2).cacheBlock(key, block, false, false);
+  }
+
+  /**
+   * Verifies that inclusive topology construction disables legacy L1 
victim-cache delegation.
+   * <p>
+   * Inclusive combined-cache construction may wire L1 to L2 before the cache 
is adapted to the
+   * topology-backed model. The topology-backed inclusive service needs L1 and 
L2 as independent
+   * engines so L2 lookup and promotion policy are applied by the topology.
+   * </p>
+   */
+  @Test
+  void testTieredInclusiveFactoryUnsetsL1VictimCache() {
+    FirstLevelBlockCache l1 = mock(FirstLevelBlockCache.class);
+    BlockCache l2 = mock(BlockCache.class);
+
+    
TopologyBackedCacheAccessServices.fromTieredInclusiveBlockCaches("inclusive-combined",
 l1, l2,
+      noPromotionPolicy());
+
+    verify(l1).unsetVictimCache();
+  }
+
+  /**
+   * Creates a topology-backed cache access service using a tiered inclusive 
topology.
+   * @param l1     first-level block cache
+   * @param l2     second-level block cache
+   * @param policy cache placement and admission policy
+   * @return topology-backed cache access service using the inclusive topology 
path
+   */
+  private static TopologyBackedCacheAccessService service(BlockCache l1, 
BlockCache l2,
+    CachePlacementAdmissionPolicy policy) {
+    return 
TopologyBackedCacheAccessServices.fromTieredInclusiveBlockCaches("inclusive-combined",
+      l1, l2, policy);
+  }
+
+  /**
+   * Creates a cache request context used by read-path tests.
+   * <p>
+   * The returned context enables caching, marks the request as non-repeat, 
and asks the cache to
+   * update cache metrics. These values match the read-path behavior covered 
by the topology-backed
+   * cache access service tests.
+   * </p>
+   * @return cache request context for read-path tests
+   */
+  private static CacheRequestContext requestContext() {
+    return CacheRequestContext.newBuilder().withCaching(true).withRepeat(false)
+      .withUpdateCacheMetrics(true).build();
+  }
+
+  /**
+   * Creates a cache write context used by cache population tests.
+   * <p>
+   * The returned context uses the default non-in-memory and non-blocking 
write behavior expected by
+   * the existing compatibility tests.
+   * </p>
+   * @return cache write context for cache population tests
+   */
+  private static CacheWriteContext writeContext() {
+    return 
CacheWriteContext.newBuilder().withInMemory(false).withWaitWhenCache(false).build();
+  }
+
+  /**
+   * Creates a placement policy that never promotes a block after a cache hit.
+   * <p>
+   * This policy is useful for lookup tests that need to verify only the 
lookup order and returned
+   * block without introducing promotion side effects.
+   * </p>
+   * @return cache placement and admission policy that disables promotion
+   */
+  private static CachePlacementAdmissionPolicy noPromotionPolicy() {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(policy.shouldPromote(any(), any(), any(), any(), any()))
+      .thenReturn(PromotionDecision.none());
+    return policy;
+  }
+
+  /**
+   * Creates a placement policy that promotes an L2 hit to L1.
+   * <p>
+   * The policy returns no promotion for L1 hits and requests promotion to L1 
for L2 hits. In an
+   * inclusive topology, this should copy the block into L1 without evicting 
it from L2.
+   * </p>
+   * @return cache placement and admission policy that promotes L2 hits to L1
+   */
+  private static CachePlacementAdmissionPolicy promoteL2HitToL1Policy() {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(policy.shouldPromote(any(), any(), eq(CacheTier.L1), any(), any()))
+      .thenReturn(PromotionDecision.none());
+    when(policy.shouldPromote(any(), any(), eq(CacheTier.L2), any(), any()))
+      .thenReturn(PromotionDecision.promoteTo(CacheTier.L1, false));
+    return policy;
+  }
+
+  /**
+   * Creates a placement policy that admits writes to the supplied tiers.
+   * <p>
+   * The returned policy admits every block and returns a multi-tier placement 
decision containing
+   * the tiers supplied by the caller.
+   * </p>
+   * @param tiers cache tiers selected by the policy
+   * @return cache placement and admission policy that writes to the supplied 
tiers
+   */
+  private static CachePlacementAdmissionPolicy admitToTiersPolicy(CacheTier... 
tiers) {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(policy.shouldAdmit(any(), any(), any(), any(), any()))
+      .thenReturn(AdmissionDecision.admit());
+    when(policy.selectTier(any(), any(), any(), any()))
+      .thenReturn(TierDecision.multiple(Arrays.asList(tiers)));
+    return policy;
+  }
+
+  /**
+   * Copies an iterable of cached blocks into a list.
+   * <p>
+   * The helper makes cached-block iterable assertions deterministic and easy 
to compare with the
+   * expected tier order.
+   * </p>
+   * @param iterable cached-block iterable to copy
+   * @return list containing all cached blocks produced by the iterable
+   */
+  private static List<CachedBlock> toList(Iterable<CachedBlock> iterable) {
+    List<CachedBlock> blocks = new ArrayList<>();
+    for (CachedBlock block : iterable) {
+      blocks.add(block);
+    }
+    return blocks;
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestSingleTierTopologyBackedCacheAccessService.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestSingleTierTopologyBackedCacheAccessService.java
new file mode 100644
index 00000000000..5a21b34b679
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestSingleTierTopologyBackedCacheAccessService.java
@@ -0,0 +1,425 @@
+/*
+ * 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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+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.CacheStats;
+import org.apache.hadoop.hbase.io.hfile.Cacheable;
+import org.apache.hadoop.hbase.io.hfile.CachedBlock;
+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;
+
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestSingleTierTopologyBackedCacheAccessService {
+
+  /**
+   * Verifies that a plain single-tier {@link BlockCache} is adapted to a 
topology-backed cache
+   * access service.
+   * <p>
+   * HBASE-30329 routes legacy block caches through {@link 
TopologyBackedCacheAccessService}. A
+   * plain non-combined block cache should be represented by {@link 
SingleTierTopology}, not by the
+   * old {@link BlockCacheBackedCacheAccessService} runtime path.
+   * </p>
+   */
+  @Test
+  void testSingleTierBlockCacheUsesTopologyBackedAccessService() {
+    BlockCache blockCache = mock(BlockCache.class);
+
+    CacheAccessService service = 
CacheAccessServices.fromBlockCache(blockCache);
+
+    assertTrue(service instanceof TopologyBackedCacheAccessService);
+    TopologyBackedCacheAccessService topologyBackedService =
+      (TopologyBackedCacheAccessService) service;
+    assertEquals(CacheTopologyType.SINGLE_TIER, 
topologyBackedService.getTopology().getType());
+  }
+
+  /**
+   * Verifies that a single-tier topology exposes the expected topology 
metadata.
+   * <p>
+   * The topology should contain exactly one engine, expose only the SINGLE 
tier, and return the
+   * same engine for {@link CacheTier#SINGLE}. Other tiers should not resolve 
to an engine.
+   * </p>
+   */
+  @Test
+  void testSingleTierTopologyMetadata() {
+    CacheEngine engine = mock(CacheEngine.class);
+    SingleTierTopology topology = new SingleTierTopology("single", engine);
+
+    assertEquals("single", topology.getName());
+    assertEquals(CacheTopologyType.SINGLE_TIER, topology.getType());
+    assertEquals(Arrays.asList(engine), topology.getEngines());
+    assertEquals(Arrays.asList(CacheTier.SINGLE), topology.getTiers());
+    assertSame(engine, topology.getEngine(CacheTier.SINGLE).orElseThrow());
+    assertFalse(topology.getEngine(CacheTier.L1).isPresent());
+    assertFalse(topology.getEngine(CacheTier.L2).isPresent());
+  }
+
+  /**
+   * Verifies that a single-tier topology-backed service reads blocks from the 
single backing cache.
+   * <p>
+   * Since there is only one tier, the access service should delegate the read 
to the L1 engine and
+   * return the block supplied by the wrapped block cache.
+   * </p>
+   */
+  @Test
+  void testGetBlockDelegatesToSingleTier() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    when(blockCache.getBlock(key, true, false, true)).thenReturn(block);
+
+    TopologyBackedCacheAccessService service = service(blockCache, 
noPromotionPolicy());
+
+    assertSame(block, service.getBlock(key, requestContext()));
+
+    verify(blockCache).getBlock(key, true, false, true);
+  }
+
+  /**
+   * Verifies that a single-tier topology-backed service returns {@code null} 
when the backing cache
+   * misses.
+   * <p>
+   * The service should not attempt any tier fallback because the topology 
contains only one cache
+   * engine.
+   * </p>
+   */
+  @Test
+  void testGetBlockReturnsNullOnMiss() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+
+    when(blockCache.getBlock(key, true, false, true)).thenReturn(null);
+
+    TopologyBackedCacheAccessService service = service(blockCache, 
noPromotionPolicy());
+
+    assertSame(null, service.getBlock(key, requestContext()));
+
+    verify(blockCache).getBlock(key, true, false, true);
+  }
+
+  /**
+   * Verifies that cache population for a single-tier topology writes to the 
single backing cache.
+   * <p>
+   * The placement policy selects L1. Since single-tier topology exposes only 
L1, the service should
+   * delegate the cache write to the wrapped block cache.
+   * </p>
+   */
+  @Test
+  void testCacheBlockDelegatesToSingleTier() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    TopologyBackedCacheAccessService service =
+      service(blockCache, admitToTiersPolicy(CacheTier.SINGLE));
+
+    service.cacheBlock(key, block, writeContext());
+
+    verify(blockCache).cacheBlock(key, block, false, false);
+  }
+
+  /**
+   * Verifies that a rejected block is not written to the single backing cache.
+   * <p>
+   * When the placement and admission policy rejects the write, the 
topology-backed service should
+   * not call any {@code cacheBlock} overload on the wrapped block cache.
+   * </p>
+   */
+  @Test
+  void testRejectedBlockIsNotCached() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    TopologyBackedCacheAccessService service = service(blockCache, 
rejectPolicy());
+
+    service.cacheBlock(key, block, writeContext());
+
+    verify(blockCache, never()).cacheBlock(any(), any());
+    verify(blockCache, never()).cacheBlock(any(), any(), anyBoolean(), 
anyBoolean());
+  }
+
+  /**
+   * Verifies that service-level eviction for a single-tier topology delegates 
to the backing cache.
+   * <p>
+   * A single-tier topology has only one possible resident tier, so eviction 
should be a direct
+   * delegation to that tier.
+   * </p>
+   */
+  @Test
+  void testEvictBlockDelegatesToSingleTier() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+
+    when(blockCache.evictBlock(key)).thenReturn(true);
+
+    TopologyBackedCacheAccessService service = service(blockCache, 
noPromotionPolicy());
+
+    assertTrue(service.evictBlock(key));
+
+    verify(blockCache).evictBlock(key);
+  }
+
+  /**
+   * Verifies that single-tier cached-block iteration is exposed through the 
topology-backed access
+   * service.
+   * <p>
+   * Diagnostic callers use {@link 
CacheAccessServices#asCachedBlockIterable(CacheAccessService)}
+   * rather than unwrapping the legacy block cache. The single-tier 
topology-backed path should
+   * expose the same cached blocks as the wrapped block cache.
+   * </p>
+   */
+  @Test
+  void testCachedBlockIterableDelegatesToSingleTier() {
+    BlockCache blockCache = mock(BlockCache.class);
+    CachedBlock firstBlock = mock(CachedBlock.class);
+    CachedBlock secondBlock = mock(CachedBlock.class);
+
+    when(blockCache.iterator()).thenReturn(Arrays.asList(firstBlock, 
secondBlock).iterator());
+
+    TopologyBackedCacheAccessService service = service(blockCache, 
noPromotionPolicy());
+
+    Optional<Iterable<CachedBlock>> iterable = 
CacheAccessServices.asCachedBlockIterable(service);
+
+    assertTrue(iterable.isPresent());
+    assertEquals(Arrays.asList(firstBlock, secondBlock), 
toList(iterable.get()));
+  }
+
+  /**
+   * Verifies that single-tier topology statistics are delegated to the 
backing engine.
+   * <p>
+   * The topology itself does not aggregate multiple tiers, so its statistics 
should be exactly the
+   * statistics exposed by the single cache engine.
+   * </p>
+   */
+  @Test
+  void testTopologyStatsDelegatesToSingleEngine() {
+    CacheEngine engine = mock(CacheEngine.class);
+    CacheStats stats = mock(CacheStats.class);
+
+    when(engine.getStats()).thenReturn(stats);
+
+    SingleTierTopology topology = new SingleTierTopology("single", engine);
+
+    assertSame(stats, topology.getStats());
+  }
+
+  /**
+   * Verifies that promotion is not supported by a single-tier topology.
+   * <p>
+   * Promotion requires a source tier and a different target tier. Since this 
topology has only one
+   * tier, promotion is a no-op and should return {@code false}.
+   * </p>
+   */
+  @Test
+  void testSingleTierTopologyDoesNotPromote() {
+    CacheEngine engine = mock(CacheEngine.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+    SingleTierTopology topology = new SingleTierTopology("single", engine);
+
+    assertFalse(topology.promote(key, block, engine, engine));
+
+    verify(engine, never()).cacheBlock(any(), any());
+    verify(engine, never()).evictBlock(any());
+  }
+
+  /**
+   * Verifies that demotion is not supported by a single-tier topology.
+   * <p>
+   * Demotion requires a source tier and a different lower target tier. Since 
this topology has only
+   * one tier, demotion is a no-op and should return {@code false}.
+   * </p>
+   */
+  @Test
+  void testSingleTierTopologyDoesNotDemote() {
+    CacheEngine engine = mock(CacheEngine.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+    SingleTierTopology topology = new SingleTierTopology("single", engine);
+
+    assertFalse(topology.demote(key, block, engine, engine));
+
+    verify(engine, never()).cacheBlock(any(), any());
+    verify(engine, never()).evictBlock(any());
+  }
+
+  /**
+   * Verifies that shutting down a single-tier topology-backed service shuts 
down the backing cache.
+   * <p>
+   * The topology-backed service owns the topology lifecycle. For a 
single-tier topology, shutdown
+   * should be delegated to the only backing engine.
+   * </p>
+   */
+  @Test
+  void testShutdownDelegatesToSingleTier() {
+    BlockCache blockCache = mock(BlockCache.class);
+
+    TopologyBackedCacheAccessService service = service(blockCache, 
noPromotionPolicy());
+
+    service.shutdown();
+
+    verify(blockCache).shutdown();
+  }
+
+  /**
+   * Verifies that single-tier topology-backed cache access preserves legacy 
current-size reporting.
+   * <p>
+   * The old {@link BlockCacheBackedCacheAccessService} path delegated
+   * {@link CacheAccessService#getCurrentSize()} to {@link 
BlockCache#getCurrentSize()}. After
+   * routing plain block caches through {@link 
TopologyBackedCacheAccessService}, the same value
+   * must still be reported through the cache access service. This is 
intentionally different from
+   * {@link CacheAccessService#getCurrentDataSize()} because some cache 
implementations include
+   * metadata or allocator overhead in their current size.
+   * </p>
+   */
+  @Test
+  void testSingleTierCurrentSizeDelegatesToBlockCacheCurrentSize() {
+    BlockCache blockCache = mock(BlockCache.class);
+
+    when(blockCache.getCurrentSize()).thenReturn(1234L);
+    when(blockCache.getCurrentDataSize()).thenReturn(1000L);
+
+    TopologyBackedCacheAccessService service = service(blockCache, 
noPromotionPolicy());
+
+    assertEquals(1234L, service.getCurrentSize());
+    assertEquals(1000L, service.getCurrentDataSize());
+  }
+
+  /**
+   * Creates a topology-backed cache access service using a single-tier 
topology.
+   * @param blockCache legacy block cache backing the single tier
+   * @param policy     cache placement and admission policy
+   * @return topology-backed cache access service using a single-tier topology
+   */
+  private static TopologyBackedCacheAccessService service(BlockCache 
blockCache,
+    CachePlacementAdmissionPolicy policy) {
+    return TopologyBackedCacheAccessServices.fromSingleBlockCache("single", 
blockCache, policy);
+  }
+
+  /**
+   * Creates a cache request context used by read-path tests.
+   * <p>
+   * The returned context enables caching, marks the request as non-repeat, 
and asks the cache to
+   * update cache metrics. These values match the read-path behavior covered 
by the topology-backed
+   * cache access service tests.
+   * </p>
+   * @return cache request context for read-path tests
+   */
+  private static CacheRequestContext requestContext() {
+    return CacheRequestContext.newBuilder().withCaching(true).withRepeat(false)
+      .withUpdateCacheMetrics(true).build();
+  }
+
+  /**
+   * Creates a cache write context used by cache population tests.
+   * <p>
+   * The returned context uses the default non-in-memory and non-blocking 
write behavior expected by
+   * the existing topology-backed cache access service tests.
+   * </p>
+   * @return cache write context for cache population tests
+   */
+  private static CacheWriteContext writeContext() {
+    return 
CacheWriteContext.newBuilder().withInMemory(false).withWaitWhenCache(false).build();
+  }
+
+  /**
+   * Creates a placement policy that never promotes a block after a cache hit.
+   * <p>
+   * This policy is useful for lookup tests that need to verify only lookup 
delegation and returned
+   * block behavior without introducing promotion side effects.
+   * </p>
+   * @return cache placement and admission policy that disables promotion
+   */
+  private static CachePlacementAdmissionPolicy noPromotionPolicy() {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(policy.shouldPromote(any(), any(), any(), any(), any()))
+      .thenReturn(PromotionDecision.none());
+    return policy;
+  }
+
+  /**
+   * Creates a placement policy that admits writes to the supplied tiers.
+   * <p>
+   * The returned policy admits every block and returns a multi-tier placement 
decision containing
+   * the tiers supplied by the caller.
+   * </p>
+   * @param tiers cache tiers selected by the policy
+   * @return cache placement and admission policy that writes to the supplied 
tiers
+   */
+  private static CachePlacementAdmissionPolicy admitToTiersPolicy(CacheTier... 
tiers) {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(policy.shouldAdmit(any(), any(), any(), any(), any()))
+      .thenReturn(AdmissionDecision.admit());
+    when(policy.selectTier(any(), any(), any(), any()))
+      .thenReturn(TierDecision.multiple(Arrays.asList(tiers)));
+    return policy;
+  }
+
+  /**
+   * Creates a placement policy that rejects every block.
+   * <p>
+   * The returned policy is used to verify that rejected cache writes are not 
delegated to the
+   * backing cache.
+   * </p>
+   * @return cache placement and admission policy that rejects every block
+   */
+  private static CachePlacementAdmissionPolicy rejectPolicy() {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(policy.shouldAdmit(any(), any(), any(), any(), any()))
+      .thenReturn(AdmissionDecision.reject("test rejection"));
+    return policy;
+  }
+
+  /**
+   * Copies an iterable of cached blocks into a list.
+   * <p>
+   * The helper makes cached-block iterable assertions deterministic and easy 
to compare with the
+   * expected order.
+   * </p>
+   * @param iterable cached-block iterable to copy
+   * @return list containing all cached blocks produced by the iterable
+   */
+  private static List<CachedBlock> toList(Iterable<CachedBlock> iterable) {
+    List<CachedBlock> blocks = new ArrayList<>();
+    for (CachedBlock block : iterable) {
+      blocks.add(block);
+    }
+    return blocks;
+  }
+}

Reply via email to