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 b8cb97f1e10 HBASE-30024 Add BlockCache-backed CacheEngine adapter for 
TieredExclusiveTopology (#8476)
b8cb97f1e10 is described below

commit b8cb97f1e100d3be02dc78e36477f189b457ecef
Author: Vladimir Rodionov <[email protected]>
AuthorDate: Fri Jul 17 14:03:38 2026 -0700

    HBASE-30024 Add BlockCache-backed CacheEngine adapter for 
TieredExclusiveTopology (#8476)
    
    Signed-off-by: Andor Molnár <[email protected]>
    Signed-off-by: Tak Lon (Stephen) Wu <[email protected]>
---
 .../hfile/cache/BlockCacheBackedCacheEngine.java   | 220 +++++++++++++++++++
 .../hadoop/hbase/io/hfile/cache/CacheEngines.java  |  48 +++++
 .../cache/TestBlockCacheBackedCacheEngine.java     | 220 +++++++++++++++++++
 .../hbase/io/hfile/cache/TestCacheEngines.java     |  49 +++++
 ...heAccessServiceWithBlockCacheBackedEngines.java | 232 +++++++++++++++++++++
 5 files changed, 769 insertions(+)

diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheEngine.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheEngine.java
new file mode 100644
index 00000000000..98fca26b46e
--- /dev/null
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/BlockCacheBackedCacheEngine.java
@@ -0,0 +1,220 @@
+/*
+ * 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 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.yetus.audience.InterfaceAudience;
+
+/**
+ * {@link CacheEngine} adapter backed by an existing {@link BlockCache} 
implementation.
+ * <p>
+ * This adapter is a compatibility bridge for the pluggable block cache 
migration. It allows
+ * existing {@link BlockCache} implementations such as {@code LruBlockCache}, 
{@code BucketCache},
+ * {@code TinyLfuBlockCache}, or {@code LruAdaptiveBlockCache} to participate 
in
+ * {@link CacheTopology} implementations before those caches are migrated to 
implement
+ * {@link CacheEngine} directly.
+ * </p>
+ * <p>
+ * The adapter does not add placement, admission, promotion, or topology 
behavior. It delegates
+ * storage-oriented operations to the wrapped {@link BlockCache}. Topology 
orchestration remains the
+ * responsibility of {@link CacheTopology}, and admission or placement 
decisions remain the
+ * responsibility of {@link CachePlacementAdmissionPolicy}.
+ * </p>
+ */
[email protected]
+public class BlockCacheBackedCacheEngine implements CacheEngine {
+
+  private final BlockCache blockCache;
+
+  /**
+   * Creates a {@link CacheEngine} backed by the given {@link BlockCache}.
+   * @param blockCache legacy block cache to adapt
+   */
+  public BlockCacheBackedCacheEngine(BlockCache blockCache) {
+    this.blockCache = Objects.requireNonNull(blockCache, "blockCache must not 
be null");
+  }
+
+  /**
+   * Returns the wrapped legacy {@link BlockCache}.
+   * <p>
+   * This accessor is intended for tests, diagnostics, and transitional wiring 
only.
+   * </p>
+   * @return wrapped block cache
+   */
+  public BlockCache getBlockCache() {
+    return blockCache;
+  }
+
+  @Override
+  public String getName() {
+    return blockCache.getClass().getSimpleName();
+  }
+
+  @Override
+  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean 
inMemory) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(buf, "buf must not be null");
+    blockCache.cacheBlock(cacheKey, buf, inMemory);
+  }
+
+  @Override
+  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean 
inMemory,
+    boolean waitWhenCache) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(buf, "buf must not be null");
+    blockCache.cacheBlock(cacheKey, buf, inMemory, waitWhenCache);
+  }
+
+  @Override
+  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(buf, "buf must not be null");
+    blockCache.cacheBlock(cacheKey, buf);
+  }
+
+  @Override
+  public Cacheable getBlock(BlockCacheKey cacheKey, boolean caching, boolean 
repeat,
+    boolean updateCacheMetrics) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    return blockCache.getBlock(cacheKey, caching, repeat, updateCacheMetrics);
+  }
+
+  @Override
+  public Cacheable getBlock(BlockCacheKey cacheKey, boolean caching, boolean 
repeat,
+    boolean updateCacheMetrics, BlockType blockType) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    Objects.requireNonNull(blockType, "blockType must not be null");
+    return blockCache.getBlock(cacheKey, caching, repeat, updateCacheMetrics, 
blockType);
+  }
+
+  @Override
+  public boolean evictBlock(BlockCacheKey cacheKey) {
+    Objects.requireNonNull(cacheKey, "cacheKey must not be null");
+    return blockCache.evictBlock(cacheKey);
+  }
+
+  @Override
+  public int evictBlocksByHfileName(String hfileName) {
+    Objects.requireNonNull(hfileName, "hfileName must not be null");
+    return blockCache.evictBlocksByHfileName(hfileName);
+  }
+
+  @Override
+  public int evictBlocksRangeByHfileName(String hfileName, long initOffset, 
long endOffset) {
+    Objects.requireNonNull(hfileName, "hfileName must not be null");
+    return blockCache.evictBlocksRangeByHfileName(hfileName, initOffset, 
endOffset);
+  }
+
+  @Override
+  public int evictBlocksByRegionName(String regionName) {
+    Objects.requireNonNull(regionName, "regionName must not be null");
+    // BlockCache does not support eviction by region name,
+    // so we return 0 to indicate no blocks were evicted.
+    return 0;
+  }
+
+  @Override
+  public CacheStats getStats() {
+    return blockCache.getStats();
+  }
+
+  @Override
+  public void shutdown() {
+    blockCache.shutdown();
+  }
+
+  @Override
+  public long getMaxSize() {
+    return blockCache.getMaxSize();
+  }
+
+  @Override
+  public long getFreeSize() {
+    return blockCache.getFreeSize();
+  }
+
+  @Override
+  public long size() {
+    return blockCache.size();
+  }
+
+  @Override
+  public long getCurrentDataSize() {
+    return blockCache.getCurrentDataSize();
+  }
+
+  @Override
+  public long getBlockCount() {
+    return blockCache.getBlockCount();
+  }
+
+  @Override
+  public long getDataBlockCount() {
+    return blockCache.getDataBlockCount();
+  }
+
+  @Override
+  public Optional<Boolean> blockFitsIntoTheCache(HFileBlock block) {
+    Objects.requireNonNull(block, "block must not be null");
+    return blockCache.blockFitsIntoTheCache(block);
+  }
+
+  @Override
+  public Optional<Boolean> isAlreadyCached(BlockCacheKey key) {
+    Objects.requireNonNull(key, "key must not be null");
+    return blockCache.isAlreadyCached(key);
+  }
+
+  @Override
+  public Optional<Integer> getBlockSize(BlockCacheKey key) {
+    Objects.requireNonNull(key, "key must not be null");
+    return blockCache.getBlockSize(key);
+  }
+
+  @Override
+  public boolean isCacheEnabled() {
+    return blockCache.isCacheEnabled();
+  }
+
+  @Override
+  public boolean waitForCacheInitialization(long timeout) {
+    return blockCache.waitForCacheInitialization(timeout);
+  }
+
+  @Override
+  public void onConfigurationChange(Configuration config) {
+    Objects.requireNonNull(config, "config must not be null");
+    blockCache.onConfigurationChange(config);
+  }
+
+  @Override
+  public void notifyFileCachingCompleted(Path fileName, int totalBlockCount, 
int dataBlockCount,
+    long size) {
+    Objects.requireNonNull(fileName, "fileName must not be null");
+    blockCache.notifyFileCachingCompleted(fileName, totalBlockCount, 
dataBlockCount, size);
+  }
+}
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEngines.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEngines.java
new file mode 100644
index 00000000000..a9707b11676
--- /dev/null
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/CacheEngines.java
@@ -0,0 +1,48 @@
+/*
+ * 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 {@link CacheEngine} instances.
+ * <p>
+ * These helpers are intended to keep transitional wiring concise while 
existing cache
+ * implementations are still based on {@link BlockCache}. Once built-in caches 
implement
+ * {@link CacheEngine} directly, callers can construct or obtain those engines 
without going through
+ * a legacy adapter.
+ * </p>
+ */
[email protected]
+public final class CacheEngines {
+
+  private CacheEngines() {
+  }
+
+  /**
+   * Wraps an existing {@link BlockCache} as a {@link CacheEngine}.
+   * @param blockCache block cache to adapt
+   * @return cache engine backed by the supplied block cache
+   */
+  public static CacheEngine fromBlockCache(BlockCache blockCache) {
+    Objects.requireNonNull(blockCache, "blockCache must not be null");
+    return new BlockCacheBackedCacheEngine(blockCache);
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestBlockCacheBackedCacheEngine.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestBlockCacheBackedCacheEngine.java
new file mode 100644
index 00000000000..13384cf6499
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestBlockCacheBackedCacheEngine.java
@@ -0,0 +1,220 @@
+/*
+ * 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.assertThrows;
+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;
+
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestBlockCacheBackedCacheEngine {
+
+  @Test
+  void testRejectsNullBlockCache() {
+    assertThrows(NullPointerException.class, () -> new 
BlockCacheBackedCacheEngine(null));
+  }
+
+  @Test
+  void testReturnsWrappedBlockCache() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+
+    assertSame(blockCache, engine.getBlockCache());
+  }
+
+  @Test
+  void testGetNameUsesWrappedClassName() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+
+    assertEquals(blockCache.getClass().getSimpleName(), engine.getName());
+  }
+
+  @Test
+  void testCacheBlockDelegates() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    engine.cacheBlock(key, block);
+    engine.cacheBlock(key, block, true);
+    engine.cacheBlock(key, block, true, true);
+
+    verify(blockCache).cacheBlock(key, block);
+    verify(blockCache).cacheBlock(key, block, true);
+    verify(blockCache).cacheBlock(key, block, true, true);
+  }
+
+  @Test
+  void testGetBlockDelegates() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    when(blockCache.getBlock(key, true, false, true)).thenReturn(block);
+    when(blockCache.getBlock(key, true, false, true, 
BlockType.DATA)).thenReturn(block);
+
+    assertSame(block, engine.getBlock(key, true, false, true));
+    assertSame(block, engine.getBlock(key, true, false, true, BlockType.DATA));
+  }
+
+  @Test
+  void testEvictionDelegates() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+
+    when(blockCache.evictBlock(key)).thenReturn(true);
+    when(blockCache.evictBlocksByHfileName("file")).thenReturn(2);
+    when(blockCache.evictBlocksRangeByHfileName("file", 10L, 
20L)).thenReturn(3);
+
+    assertTrue(engine.evictBlock(key));
+    assertEquals(2, engine.evictBlocksByHfileName("file"));
+    assertEquals(3, engine.evictBlocksRangeByHfileName("file", 10L, 20L));
+  }
+
+  @Test
+  void testStatsAndSizingDelegates() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+    CacheStats stats = mock(CacheStats.class);
+
+    when(blockCache.getStats()).thenReturn(stats);
+    when(blockCache.getMaxSize()).thenReturn(100L);
+    when(blockCache.getFreeSize()).thenReturn(20L);
+    when(blockCache.size()).thenReturn(80L);
+    when(blockCache.getCurrentDataSize()).thenReturn(70L);
+    when(blockCache.getBlockCount()).thenReturn(7L);
+    when(blockCache.getDataBlockCount()).thenReturn(5L);
+
+    assertSame(stats, engine.getStats());
+    assertEquals(100L, engine.getMaxSize());
+    assertEquals(20L, engine.getFreeSize());
+    assertEquals(80L, engine.size());
+    assertEquals(70L, engine.getCurrentDataSize());
+    assertEquals(7L, engine.getBlockCount());
+    assertEquals(5L, engine.getDataBlockCount());
+  }
+
+  @Test
+  void testOptionalCapabilityDelegates() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+    HFileBlock block = mock(HFileBlock.class);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+
+    
when(blockCache.blockFitsIntoTheCache(block)).thenReturn(Optional.of(true));
+    when(blockCache.isAlreadyCached(key)).thenReturn(Optional.of(false));
+    when(blockCache.getBlockSize(key)).thenReturn(Optional.of(123));
+
+    assertEquals(Optional.of(true), engine.blockFitsIntoTheCache(block));
+    assertEquals(Optional.of(false), engine.isAlreadyCached(key));
+    assertEquals(Optional.of(123), engine.getBlockSize(key));
+  }
+
+  @Test
+  void testLifecycleDelegates() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+    Configuration conf = new Configuration(false);
+    Path fileName = new Path("/hbase/test-file");
+
+    when(blockCache.isCacheEnabled()).thenReturn(true);
+    when(blockCache.waitForCacheInitialization(1000L)).thenReturn(true);
+
+    assertTrue(engine.isCacheEnabled());
+    assertTrue(engine.waitForCacheInitialization(1000L));
+
+    engine.onConfigurationChange(conf);
+    engine.notifyFileCachingCompleted(fileName, 10, 8, 1024L);
+    engine.shutdown();
+
+    verify(blockCache).onConfigurationChange(conf);
+    verify(blockCache).notifyFileCachingCompleted(fileName, 10, 8, 1024L);
+    verify(blockCache).shutdown();
+  }
+
+  @Test
+  void testFactoryCreatesBlockCacheBackedEngine() {
+    BlockCache blockCache = mock(BlockCache.class);
+    CacheEngine engine = CacheEngines.fromBlockCache(blockCache);
+
+    assertTrue(engine instanceof BlockCacheBackedCacheEngine);
+    assertSame(blockCache, ((BlockCacheBackedCacheEngine) 
engine).getBlockCache());
+  }
+
+  @Test
+  void testFactoryRejectsNullBlockCache() {
+    assertThrows(NullPointerException.class, () -> 
CacheEngines.fromBlockCache(null));
+  }
+
+  @Test
+  void testNullArgumentsRejected() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+    BlockCacheKey key = new BlockCacheKey("file", 1L);
+    Cacheable block = mock(Cacheable.class);
+
+    assertThrows(NullPointerException.class, () -> engine.cacheBlock(null, 
block));
+    assertThrows(NullPointerException.class, () -> engine.cacheBlock(key, 
null));
+    assertThrows(NullPointerException.class, () -> engine.getBlock(null, true, 
false, true));
+    assertThrows(NullPointerException.class, () -> engine.evictBlock(null));
+    assertThrows(NullPointerException.class, () -> 
engine.evictBlocksByHfileName(null));
+    assertThrows(NullPointerException.class,
+      () -> engine.evictBlocksRangeByHfileName(null, 0L, 1L));
+    assertThrows(NullPointerException.class, () -> 
engine.evictBlocksByRegionName(null));
+    assertThrows(NullPointerException.class, () -> 
engine.blockFitsIntoTheCache(null));
+    assertThrows(NullPointerException.class, () -> 
engine.isAlreadyCached(null));
+    assertThrows(NullPointerException.class, () -> engine.getBlockSize(null));
+    assertThrows(NullPointerException.class, () -> 
engine.onConfigurationChange(null));
+    assertThrows(NullPointerException.class,
+      () -> engine.notifyFileCachingCompleted(null, 1, 1, 1L));
+  }
+
+  @Test
+  void testDisabledCacheStateDelegates() {
+    BlockCache blockCache = mock(BlockCache.class);
+    BlockCacheBackedCacheEngine engine = new 
BlockCacheBackedCacheEngine(blockCache);
+
+    when(blockCache.isCacheEnabled()).thenReturn(false);
+
+    assertFalse(engine.isCacheEnabled());
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheEngines.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheEngines.java
new file mode 100644
index 00000000000..bd9d0bb49c1
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestCacheEngines.java
@@ -0,0 +1,49 @@
+/*
+ * 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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+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)
+class TestCacheEngines {
+
+  @Test
+  void testFromBlockCacheReturnsBlockCacheBackedCacheEngine() {
+    BlockCache blockCache = mock(BlockCache.class);
+
+    CacheEngine engine = CacheEngines.fromBlockCache(blockCache);
+
+    assertTrue(engine instanceof BlockCacheBackedCacheEngine);
+    assertSame(blockCache, ((BlockCacheBackedCacheEngine) 
engine).getBlockCache());
+  }
+
+  @Test
+  void testFromBlockCacheRejectsNull() {
+    assertThrows(NullPointerException.class, () -> 
CacheEngines.fromBlockCache(null));
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines.java
new file mode 100644
index 00000000000..02d39623103
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/io/hfile/cache/TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines.java
@@ -0,0 +1,232 @@
+/*
+ * 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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+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.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;
+
+@Tag(IOTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestTopologyBackedCacheAccessServiceWithBlockCacheBackedEngines {
+
+  @Test
+  void testL1HitDoesNotCheckL2() {
+    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(), any(Boolean.class), 
any(Boolean.class), any(Boolean.class));
+  }
+
+  @Test
+  void testL2HitPromotesToL1AndEvictsFromL2() {
+    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, 
promoteToL1Policy());
+
+    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).evictBlock(key);
+  }
+
+  @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 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(), any(Boolean.class), 
any(Boolean.class));
+  }
+
+  @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(), any(Boolean.class), 
any(Boolean.class));
+  }
+
+  @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 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(), any(Boolean.class), 
any(Boolean.class));
+    verify(l2, never()).cacheBlock(any(), any(), any(Boolean.class), 
any(Boolean.class));
+  }
+
+  @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) {
+    CacheEngine l1Engine = CacheEngines.fromBlockCache(l1);
+    CacheEngine l2Engine = CacheEngines.fromBlockCache(l2);
+    CacheTopology topology = new TieredExclusiveTopology("test", l1Engine, 
l2Engine);
+    return new TopologyBackedCacheAccessService(topology, 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 promoteToL1Policy() {
+    CachePlacementAdmissionPolicy policy = 
mock(CachePlacementAdmissionPolicy.class);
+    when(policy.shouldPromote(any(), any(), eq(CacheTier.L2), any(), any()))
+      .thenReturn(PromotionDecision.promoteTo(CacheTier.L1, false));
+    when(policy.shouldPromote(any(), any(), eq(CacheTier.L1), any(), any()))
+      .thenReturn(PromotionDecision.none());
+    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;
+  }
+}

Reply via email to