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 000000000000..98fca26b46e2 --- /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. + *

+ * 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. + *

+ *

+ * 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}. + *

+ */ +@InterfaceAudience.Private +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}. + *

+ * This accessor is intended for tests, diagnostics, and transitional wiring only. + *

+ * @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 blockFitsIntoTheCache(HFileBlock block) { + Objects.requireNonNull(block, "block must not be null"); + return blockCache.blockFitsIntoTheCache(block); + } + + @Override + public Optional isAlreadyCached(BlockCacheKey key) { + Objects.requireNonNull(key, "key must not be null"); + return blockCache.isAlreadyCached(key); + } + + @Override + public Optional 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 000000000000..a9707b11676a --- /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. + *

+ * 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. + *

+ */ +@InterfaceAudience.Private +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 000000000000..13384cf6499e --- /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 000000000000..bd9d0bb49c11 --- /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 000000000000..02d39623103f --- /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; + } +}