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