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 3ce586c22a78..8df55427ecf2 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 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;
/**
@@ -110,6 +111,155 @@ public static TopologyBackedCacheAccessService fromTieredExclusiveBlockCaches(St
return new TopologyBackedCacheAccessService(topology, policy);
}
+ /**
+ * Creates a topology-backed cache access service for an {@link InclusiveCombinedBlockCache}.
+ *
+ * {@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 parameters() throws IOException {
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 void setUp() throws IOException {
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 @@ private void testCachingDataBlocksDuringCompactionInternals(boolean useTags,
.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 @@ private void testCachingDataBlocksDuringCompactionInternals(boolean useTags,
// 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 37dbc0a92278..368c72617bb1 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
@@ -663,11 +663,7 @@ public static CacheAccessServiceTestInstance bucketInstance(String
*/
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 8a7378c7b837..000000000000
--- 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 1c548b9f9304..65dff577bf3e 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/TestInclusiveCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestInclusiveCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
new file mode 100644
index 000000000000..42c468800d60
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestInclusiveCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
@@ -0,0 +1,356 @@
+/*
+ * 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.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}.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * This covers the inclusive cache residency model where the same block can be intentionally
+ * present in multiple tiers.
+ *
+ */
+ @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.
+ *
+ * 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.
+ *
+ */
+ @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 = 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.
+ *
+ * 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.
+ *
+ */
+ @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();
+ }
+
+ /**
+ * 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.
+ *
+ * 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.
+ *
+ * @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.
+ *
+ * The returned context uses the default non-in-memory and non-blocking write behavior expected by
+ * the existing compatibility tests.
+ *
+ * @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.
+ *
+ * This policy is useful for lookup tests that need to verify only the lookup order and returned
+ * block without introducing promotion side effects.
+ *
+ * @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.
+ *
+ * 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.
+ *
+ * @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.
+ *
+ * The returned policy admits every block and returns a multi-tier placement decision containing
+ * the tiers supplied by the caller.
+ *
+ * @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.
+ *
+ * The helper makes cached-block iterable assertions deterministic and easy to compare with the
+ * expected tier order.
+ *
+ * @param iterable cached-block iterable to copy
+ * @return list containing all cached blocks produced by the iterable
+ */
+ private static List toList(Iterable iterable) {
+ List 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 000000000000..e9117b3b3f47
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestSingleTierTopologyBackedCacheAccessService.java
@@ -0,0 +1,400 @@
+/*
+ * 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.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * 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.
+ *
+ */
+ @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.L1), topology.getTiers());
+ assertSame(engine, topology.getEngine(CacheTier.L1).orElseThrow());
+ assertFalse(topology.getEngine(CacheTier.L2).isPresent());
+ }
+
+ /**
+ * Verifies that a single-tier topology-backed service reads blocks from the single backing cache.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * The service should not attempt any tier fallback because the topology contains only one cache
+ * engine.
+ *
+ */
+ @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.
+ *
+ * The placement policy selects L1. Since single-tier topology exposes only L1, the service should
+ * delegate the cache write to the wrapped block cache.
+ *
+ */
+ @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.L1));
+
+ 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.
+ *
+ * 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.
+ *
+ */
+ @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.
+ *
+ * A single-tier topology has only one possible resident tier, so eviction should be a direct
+ * delegation to that tier.
+ *
+ */
+ @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.
+ *
+ * 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.
+ *
+ */
+ @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 = 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.
+ *
+ * The topology itself does not aggregate multiple tiers, so its statistics should be exactly the
+ * statistics exposed by the single cache engine.
+ *
+ */
+ @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.
+ *
+ * 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}.
+ *
+ */
+ @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.
+ *
+ * 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}.
+ *
+ */
+ @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.
+ *
+ * The topology-backed service owns the topology lifecycle. For a single-tier topology, shutdown
+ * should be delegated to the only backing engine.
+ *
+ */
+ @Test
+ void testShutdownDelegatesToSingleTier() {
+ BlockCache blockCache = mock(BlockCache.class);
+
+ TopologyBackedCacheAccessService service = service(blockCache, noPromotionPolicy());
+
+ service.shutdown();
+
+ verify(blockCache).shutdown();
+ }
+
+ /**
+ * 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.
+ *
+ * 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.
+ *
+ * @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.
+ *
+ * The returned context uses the default non-in-memory and non-blocking write behavior expected by
+ * the existing topology-backed cache access service tests.
+ *
+ * @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.
+ *
+ * This policy is useful for lookup tests that need to verify only lookup delegation and returned
+ * block behavior without introducing promotion side effects.
+ *
+ * @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.
+ *
+ * The returned policy admits every block and returns a multi-tier placement decision containing
+ * the tiers supplied by the caller.
+ *
+ * @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.
+ *
+ * The returned policy is used to verify that rejected cache writes are not delegated to the
+ * backing cache.
+ *
+ * @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.
+ *
+ * The helper makes cached-block iterable assertions deterministic and easy to compare with the
+ * expected order.
+ *
+ * @param iterable cached-block iterable to copy
+ * @return list containing all cached blocks produced by the iterable
+ */
+ private static List toList(Iterable iterable) {
+ List blocks = new ArrayList<>();
+ for (CachedBlock block : iterable) {
+ blocks.add(block);
+ }
+ return blocks;
+ }
+}