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));
+  }
+}

Reply via email to