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 fa69b77f5ecb..589a97bb3549 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.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 CacheConfig(Configuration conf, ColumnFamilyDescriptor family, CacheAcces 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}. + *
+ * 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. + *
+ * @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- * 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}. *
- * @param blockCache block cache to expose through {@link CacheAccessService} - * @return cache access service + *+ * {@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. + *
+ * @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 static CacheAccessService fromBlockCache(BlockCache blockCache) { * 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. * *
@@ -140,15 +152,6 @@ public static CacheAccessService disabled() {
* @return optional iterable cached-block view
* @throws NullPointerException if {@code cacheAccessService} is {@code null}
*/
- @SuppressWarnings("unchecked")
- // public static Optional
+ * 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.
+ *
+ * This topology allows plain block caches to use the same {@link TopologyBackedCacheAccessService}
+ * path as combined caches while preserving the existing cache implementation underneath.
+ *
+ * Single-tier topology has no higher or lower tier, so promotion is not supported. The method
+ * returns {@code false} without modifying the cache.
+ *
+ * Single-tier topology has no lower tier, so demotion is not supported. The method returns
+ * {@code false} without modifying the cache.
+ *
- * 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}. - *
- ** 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. *
*/ @InterfaceAudience.Private -public class TopologyBackedCacheAccessService implements CacheAccessService { +public class TopologyBackedCacheAccessService implements CacheAccessService, Iterable- * 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. *
+ * @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. *- * 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#L1}. *
- * @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+ * This method is used for tiered topologies where the placement policy decides whether the block + * belongs in L1, L2, or multiple tiers. + *
+ * @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"); @@ -626,6 +677,22 @@ public void notifyFileCachingCompleted(Path path, int blockCount, int dataBlockC } } + /** + * Returns an iterable view over cached blocks exposed by the cache engines in this topology. + *+ * 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()}. + *
+ *+ * 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()}. + *
+ * @return an aggregated cached-block iterable when at least one engine supports this capability; + * otherwise {@link Optional#empty()} + */ public Optional+ * {@link InclusiveCombinedBlockCache} represents a two-tier inclusive cache layout. Unlike the + * exclusive {@link CombinedBlockCache} path, a block may be present in more than one tier. The + * resulting service therefore uses {@link TieredInclusiveTopology}, not + * {@link TieredExclusiveTopology}. + *
+ *+ * The supplied legacy combined cache is used only as a source of the existing first-level and + * second-level block caches. Each tier is wrapped in a {@link BlockCacheBackedCacheEngine}, and + * the new {@link TopologyBackedCacheAccessService} performs access through the topology + * abstraction. + *
+ * @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 tiers + */ + public static TopologyBackedCacheAccessService + fromInclusiveCombinedBlockCache(InclusiveCombinedBlockCache combinedBlockCache) { + Objects.requireNonNull(combinedBlockCache, "combinedBlockCache must not be null"); + + BlockCache[] blockCaches = combinedBlockCache.getBlockCaches(); + if (blockCaches.length != 2) { + throw new IllegalArgumentException( + "InclusiveCombinedBlockCache must expose exactly two 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. + *+ * 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}. + *
+ *+ * 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. + *
+ * @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} + */ + 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"); + + 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}. + *+ * 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. + *
+ * @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. + *+ * 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}. + *
+ *+ * 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}. + *
+ * @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 the cache engine for the L1 tier. + * @return legacy block cache wrapped by the cache engine for the L1 tier + */ + public static BlockCache getBlockCache(CacheAccessService cacheAccessService) { + return getBlockCache(cacheAccessService, CacheTier.L1); + } + /** * Configures the legacy L1 to L2 victim-cache relationship used by CombinedBlockCache. *
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 1d7286dbe6c6..377331d00888 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.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;
@@ -491,8 +492,8 @@ void testCacheAccessServiceBackedByBlockCacheWhenBlockCacheIsConfigured() {
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 a016cbef03ab..75a03d425042 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.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 static Stream
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * This covers the inclusive cache residency model where the same block can be intentionally
+ * present in multiple tiers.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * The returned context uses the default non-in-memory and non-blocking write behavior expected by
+ * the existing compatibility tests.
+ *
+ * This policy is useful for lookup tests that need to verify only the lookup order and returned
+ * block without introducing promotion side effects.
+ *
+ * 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.
+ *
+ * The returned policy admits every block and returns a multi-tier placement decision containing
+ * the tiers supplied by the caller.
+ *
+ * The helper makes cached-block iterable assertions deterministic and easy to compare with the
+ * expected tier order.
+ *
+ * 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.
+ *
+ * The topology should contain exactly one engine, expose only the L1 tier, and return the same
+ * engine for {@link CacheTier#L1}. Other tiers should not resolve to an engine.
+ *
+ * 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.
+ *
+ * The service should not attempt any tier fallback because the topology contains only one cache
+ * engine.
+ *
+ * The placement policy selects L1. Since single-tier topology exposes only L1, the service should
+ * delegate the cache write to the wrapped block cache.
+ *
+ * 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.
+ *
+ * A single-tier topology has only one possible resident tier, so eviction should be a direct
+ * delegation to that tier.
+ *
+ * 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.
+ *
+ * The topology itself does not aggregate multiple tiers, so its statistics should be exactly the
+ * statistics exposed by the single cache engine.
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * The topology-backed service owns the topology lifecycle. For a single-tier topology, shutdown
+ * should be delegated to the only backing engine.
+ *
+ * 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.
+ *
+ * The returned context uses the default non-in-memory and non-blocking write behavior expected by
+ * the existing topology-backed cache access service tests.
+ *
+ * This policy is useful for lookup tests that need to verify only lookup delegation and returned
+ * block behavior without introducing promotion side effects.
+ *
+ * The returned policy admits every block and returns a multi-tier placement decision containing
+ * the tiers supplied by the caller.
+ *
+ * The returned policy is used to verify that rejected cache writes are not delegated to the
+ * backing cache.
+ *
+ * The helper makes cached-block iterable assertions deterministic and easy to compare with the
+ * expected order.
+ *