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 f72932df0e3 HBASE-30025 Wire TieredExclusiveTopology as
CombinedBlockCache-compatible service (#8501)
f72932df0e3 is described below
commit f72932df0e3a4501d62cfaead105f7ea0a637f9f
Author: Vladimir Rodionov <[email protected]>
AuthorDate: Mon Jul 27 19:48:08 2026 -0700
HBASE-30025 Wire TieredExclusiveTopology as CombinedBlockCache-compatible
service (#8501)
Signed-off-by: Tak Lon (Stephen) Wu <[email protected]>
---
.../cache/TopologyBackedCacheAccessServices.java | 69 +++++
...CompatibleTopologyBackedCacheAccessService.java | 280 +++++++++++++++++++++
.../TestTopologyBackedCacheAccessServices.java | 77 ++++++
3 files changed, 426 insertions(+)
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
new file mode 100644
index 00000000000..60b2b9f6ddf
--- /dev/null
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/TopologyBackedCacheAccessServices.java
@@ -0,0 +1,69 @@
+/*
+ * 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.Objects;
+import org.apache.hadoop.hbase.io.hfile.BlockCache;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Factory helpers for topology-backed {@link CacheAccessService} instances.
+ * <p>
+ * These helpers are intended for transitional wiring while existing cache
implementations still
+ * expose the legacy {@link BlockCache} API. The supplied block caches are
adapted to
+ * {@link CacheEngine} using {@link BlockCacheBackedCacheEngine}, assembled
into a
+ * {@link TieredExclusiveTopology}, and exposed through {@link
TopologyBackedCacheAccessService}.
+ * </p>
+ * <p>
+ * This class does not change production cache wiring by itself. It only
provides a reusable
+ * construction path for tests and later migration steps that need a
CombinedBlockCache-compatible
+ * topology-backed service.
+ * </p>
+ */
[email protected]
+public final class TopologyBackedCacheAccessServices {
+
+ private TopologyBackedCacheAccessServices() {
+ }
+
+ /**
+ * Creates a topology-backed cache access service from existing L1 and L2
block caches.
+ * <p>
+ * The resulting service uses {@link TieredExclusiveTopology}, which models
the current
+ * CombinedBlockCache-compatible L1/L2 behavior where promotion can move a
block from one tier to
+ * another.
+ * </p>
+ * @param name human-readable topology/service name
+ * @param l1 L1 block cache
+ * @param l2 L2 block cache
+ * @param policy placement and admission policy
+ * @return topology-backed cache access service
+ */
+ public static TopologyBackedCacheAccessService
fromTieredExclusiveBlockCaches(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 TieredExclusiveTopology(name, l1Engine,
l2Engine);
+ return new TopologyBackedCacheAccessService(topology, policy);
+ }
+}
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
new file mode 100644
index 00000000000..58db25bb9dc
--- /dev/null
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService.java
@@ -0,0 +1,280 @@
+/*
+ * 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.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+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.mockingDetails;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+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.testclassification.IOTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.mockito.invocation.Invocation;
+
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
+
+ @Test
+ void testL1HitReturnsBlockWithoutCheckingL2() {
+ 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());
+ }
+
+ @Test
+ void testL2HitReturnsBlock() {
+ 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);
+ }
+
+ @Test
+ void testMissReturnsNull() {
+ BlockCache l1 = mock(BlockCache.class);
+ BlockCache l2 = mock(BlockCache.class);
+ BlockCacheKey key = new BlockCacheKey("file", 1L);
+
+ when(l1.getBlock(key, true, false, true)).thenReturn(null);
+ when(l2.getBlock(key, true, false, true)).thenReturn(null);
+
+ TopologyBackedCacheAccessService service = service(l1, l2,
noPromotionPolicy());
+
+ assertNull(service.getBlock(key, requestContext()));
+
+ verify(l1).getBlock(key, true, false, true);
+ verify(l2).getBlock(key, true, false, true);
+ }
+
+ @Test
+ void testL2HitWithPromotionMovesBlockToL1() {
+ 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);
+ assertCachedExactlyOnce(l1, key, block);
+ verify(l2).evictBlock(key);
+ assertNotCached(l2);
+ }
+
+ @Test
+ void testRejectedBlockIsNotCached() {
+ 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, rejectPolicy());
+
+ service.cacheBlock(key, block, writeContext());
+
+ verify(l1, never()).cacheBlock(any(), any());
+ verify(l1, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
+ verify(l2, never()).cacheBlock(any(), any());
+ verify(l2, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ void testCacheBlockToL1() {
+ 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));
+
+ service.cacheBlock(key, block, writeContext());
+
+ verify(l1).cacheBlock(key, block, false, false);
+ verify(l2, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ void testCacheBlockToL2() {
+ 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.L2));
+
+ service.cacheBlock(key, block, writeContext());
+
+ verify(l2).cacheBlock(key, block, false, false);
+ verify(l1, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ void testCacheBlockToBothTiers() {
+ 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);
+ }
+
+ @Test
+ void testEvictBlockEvictsFromBothTiers() {
+ BlockCache l1 = mock(BlockCache.class);
+ BlockCache l2 = mock(BlockCache.class);
+ BlockCacheKey key = new BlockCacheKey("file", 1L);
+
+ TopologyBackedCacheAccessService service = service(l1, l2,
noPromotionPolicy());
+
+ service.evictBlock(key);
+
+ verify(l1).evictBlock(key);
+ verify(l2).evictBlock(key);
+ }
+
+ @Test
+ void testShutdownShutsDownBothTiers() {
+ BlockCache l1 = mock(BlockCache.class);
+ BlockCache l2 = mock(BlockCache.class);
+
+ TopologyBackedCacheAccessService service = service(l1, l2,
noPromotionPolicy());
+
+ service.shutdown();
+
+ verify(l1).shutdown();
+ verify(l2).shutdown();
+ }
+
+ private static TopologyBackedCacheAccessService service(BlockCache l1,
BlockCache l2,
+ CachePlacementAdmissionPolicy policy) {
+ return
TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches("combined",
l1, l2,
+ policy);
+ }
+
+ private static CacheRequestContext requestContext() {
+ return CacheRequestContext.newBuilder().withCaching(true).withRepeat(false)
+ .withUpdateCacheMetrics(true).build();
+ }
+
+ private static CacheWriteContext writeContext() {
+ return
CacheWriteContext.newBuilder().withInMemory(false).withWaitWhenCache(false).build();
+ }
+
+ private static CachePlacementAdmissionPolicy noPromotionPolicy() {
+ CachePlacementAdmissionPolicy policy =
mock(CachePlacementAdmissionPolicy.class);
+ when(policy.shouldPromote(any(), any(), any(), any(), any()))
+ .thenReturn(PromotionDecision.none());
+ return policy;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ private static CachePlacementAdmissionPolicy rejectPolicy() {
+ CachePlacementAdmissionPolicy policy =
mock(CachePlacementAdmissionPolicy.class);
+ when(policy.shouldAdmit(any(), any(), any(), any(), any()))
+ .thenReturn(AdmissionDecision.reject("test rejection"));
+ return policy;
+ }
+
+ private static void assertCachedExactlyOnce(BlockCache cache, BlockCacheKey
key,
+ Cacheable block) {
+ long cacheBlockCalls = mockingDetails(cache).getInvocations().stream()
+ .filter(invocation -> isCacheBlockInvocation(invocation, key,
block)).count();
+
+ assertEquals(1, cacheBlockCalls);
+ }
+
+ private static void assertNotCached(BlockCache cache) {
+ long cacheBlockCalls = mockingDetails(cache).getInvocations().stream()
+ .filter(invocation ->
"cacheBlock".equals(invocation.getMethod().getName())).count();
+
+ assertEquals(0, cacheBlockCalls);
+ }
+
+ private static boolean isCacheBlockInvocation(Invocation invocation,
BlockCacheKey key,
+ Cacheable block) {
+ if (!"cacheBlock".equals(invocation.getMethod().getName())) {
+ return false;
+ }
+
+ Object[] arguments = invocation.getArguments();
+ return arguments.length >= 2 && arguments[0] == key && arguments[1] ==
block;
+ }
+}
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
new file mode 100644
index 00000000000..8869d30cb55
--- /dev/null
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServices.java
@@ -0,0 +1,77 @@
+/*
+ * 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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.util.Optional;
+import org.apache.hadoop.hbase.io.hfile.BlockCache;
+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 TestTopologyBackedCacheAccessServices {
+
+ @Test
+ void testFromTieredExclusiveBlockCachesCreatesExpectedService() {
+ BlockCache l1 = mock(BlockCache.class);
+ BlockCache l2 = mock(BlockCache.class);
+ CachePlacementAdmissionPolicy policy =
mock(CachePlacementAdmissionPolicy.class);
+
+ TopologyBackedCacheAccessService service =
+
TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches("combined",
l1, l2, policy);
+
+ assertEquals("combined", service.getName());
+ assertSame(policy, service.getPolicy());
+ assertTrue(service.getTopology() instanceof TieredExclusiveTopology);
+ assertEquals(CacheTopologyType.TIERED_EXCLUSIVE,
service.getTopology().getType());
+
+ Optional<CacheEngine> l1Engine =
service.getTopology().getEngine(CacheTier.L1);
+ Optional<CacheEngine> l2Engine =
service.getTopology().getEngine(CacheTier.L2);
+
+ assertTrue(l1Engine.isPresent());
+ assertTrue(l2Engine.isPresent());
+ assertTrue(l1Engine.get() instanceof BlockCacheBackedCacheEngine);
+ assertTrue(l2Engine.get() instanceof BlockCacheBackedCacheEngine);
+ assertSame(l1, ((BlockCacheBackedCacheEngine)
l1Engine.get()).getBlockCache());
+ assertSame(l2, ((BlockCacheBackedCacheEngine)
l2Engine.get()).getBlockCache());
+ }
+
+ @Test
+ void testFromTieredExclusiveBlockCachesRejectsNullArguments() {
+ BlockCache l1 = mock(BlockCache.class);
+ BlockCache l2 = mock(BlockCache.class);
+ CachePlacementAdmissionPolicy policy =
mock(CachePlacementAdmissionPolicy.class);
+
+ assertThrows(NullPointerException.class,
+ () ->
TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches(null, l1, l2,
policy));
+ assertThrows(NullPointerException.class, () ->
TopologyBackedCacheAccessServices
+ .fromTieredExclusiveBlockCaches("combined", null, l2, policy));
+ assertThrows(NullPointerException.class, () ->
TopologyBackedCacheAccessServices
+ .fromTieredExclusiveBlockCaches("combined", l1, null, policy));
+ assertThrows(NullPointerException.class, () ->
TopologyBackedCacheAccessServices
+ .fromTieredExclusiveBlockCaches("combined", l1, l2, null));
+ }
+}