001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.io.hfile; 019 020import static org.junit.jupiter.api.Assertions.assertEquals; 021import static org.junit.jupiter.api.Assertions.assertNotNull; 022import static org.junit.jupiter.api.Assertions.assertNull; 023import static org.junit.jupiter.api.Assertions.assertTrue; 024 025import java.nio.ByteBuffer; 026import java.util.Random; 027import java.util.concurrent.ThreadLocalRandom; 028import org.apache.hadoop.hbase.io.HeapSize; 029import org.apache.hadoop.hbase.testclassification.IOTests; 030import org.apache.hadoop.hbase.testclassification.SmallTests; 031import org.apache.hadoop.hbase.util.ClassSize; 032import org.junit.jupiter.api.Tag; 033import org.junit.jupiter.api.Test; 034 035/** 036 * Tests the concurrent TinyLfuBlockCache. 037 */ 038@Tag(IOTests.TAG) 039@Tag(SmallTests.TAG) 040public class TestTinyLfuBlockCache { 041 042 @Test 043 public void testCacheSimple() throws Exception { 044 045 long maxSize = 1000000; 046 long blockSize = calculateBlockSizeDefault(maxSize, 101); 047 048 TinyLfuBlockCache cache = new TinyLfuBlockCache(maxSize, blockSize, blockSize, Runnable::run); 049 050 CachedItem[] blocks = generateRandomBlocks(100, blockSize); 051 052 long expectedCacheSize = cache.heapSize(); 053 054 // Confirm empty 055 for (CachedItem block : blocks) { 056 assertTrue(cache.getBlock(block.cacheKey, true, false, true) == null); 057 } 058 059 // Add blocks 060 for (CachedItem block : blocks) { 061 cache.cacheBlock(block.cacheKey, block); 062 expectedCacheSize += block.heapSize(); 063 } 064 065 // Verify correctly calculated cache heap size 066 assertEquals(expectedCacheSize, cache.heapSize()); 067 068 // Check if all blocks are properly cached and retrieved 069 for (CachedItem block : blocks) { 070 HeapSize buf = cache.getBlock(block.cacheKey, true, false, true); 071 assertTrue(buf != null); 072 assertEquals(buf.heapSize(), block.heapSize()); 073 } 074 075 // Re-add same blocks and ensure nothing has changed 076 long expectedBlockCount = cache.getBlockCount(); 077 for (CachedItem block : blocks) { 078 cache.cacheBlock(block.cacheKey, block); 079 } 080 assertEquals(expectedBlockCount, cache.getBlockCount(), 081 "Cache should ignore cache requests for blocks already in cache"); 082 083 // Verify correctly calculated cache heap size 084 assertEquals(expectedCacheSize, cache.heapSize()); 085 086 // Check if all blocks are properly cached and retrieved 087 for (CachedItem block : blocks) { 088 HeapSize buf = cache.getBlock(block.cacheKey, true, false, true); 089 assertTrue(buf != null); 090 assertEquals(buf.heapSize(), block.heapSize()); 091 } 092 093 // Expect no evictions 094 assertEquals(0, cache.getStats().getEvictionCount()); 095 096 CacheTestUtils.testConvertToJSON(cache); 097 } 098 099 @Test 100 public void testCacheEvictionSimple() throws Exception { 101 102 long maxSize = 100000; 103 long blockSize = calculateBlockSizeDefault(maxSize, 10); 104 105 TinyLfuBlockCache cache = new TinyLfuBlockCache(maxSize, blockSize, blockSize, Runnable::run); 106 107 CachedItem[] blocks = generateFixedBlocks(11, blockSize, "block"); 108 109 // Add all the blocks 110 for (CachedItem block : blocks) { 111 cache.cacheBlock(block.cacheKey, block); 112 } 113 114 // A single eviction run should have occurred 115 assertEquals(1, cache.getStats().getEvictionCount()); 116 117 // The cache did not grow beyond max 118 assertTrue(cache.heapSize() < maxSize); 119 120 // All blocks except one should be in the cache 121 assertEquals(10, cache.getBlockCount()); 122 } 123 124 @Test 125 public void testScanResistance() throws Exception { 126 127 long maxSize = 100000; 128 long blockSize = calculateBlockSize(maxSize, 10); 129 130 TinyLfuBlockCache cache = new TinyLfuBlockCache(maxSize, blockSize, blockSize, Runnable::run); 131 132 CachedItem[] singleBlocks = generateFixedBlocks(20, blockSize, "single"); 133 CachedItem[] multiBlocks = generateFixedBlocks(5, blockSize, "multi"); 134 135 // Add 5 blocks from each 136 for (int i = 0; i < 5; i++) { 137 cache.cacheBlock(singleBlocks[i].cacheKey, singleBlocks[i]); 138 cache.cacheBlock(multiBlocks[i].cacheKey, multiBlocks[i]); 139 } 140 141 // Add frequency 142 for (int i = 0; i < 5; i++) { 143 for (int j = 0; j < 10; j++) { 144 CachedItem block = multiBlocks[i]; 145 cache.getBlock(block.cacheKey, true, false, true); 146 } 147 } 148 149 // Let's keep "scanning" by adding single blocks. From here on we only 150 // expect evictions from the single bucket. 151 152 for (int i = 5; i < 18; i++) { 153 cache.cacheBlock(singleBlocks[i].cacheKey, singleBlocks[i]); 154 } 155 156 for (CachedItem block : multiBlocks) { 157 assertTrue(cache.cache.asMap().containsKey(block.cacheKey)); 158 } 159 160 assertEquals(10, cache.getBlockCount()); 161 assertEquals(13, cache.getStats().getEvictionCount()); 162 163 } 164 165 @Test 166 public void testMaxBlockSize() throws Exception { 167 long maxSize = 100000; 168 long blockSize = calculateBlockSize(maxSize, 10); 169 170 TinyLfuBlockCache cache = new TinyLfuBlockCache(maxSize, blockSize, blockSize, Runnable::run); 171 CachedItem[] tooLong = generateFixedBlocks(10, 2 * blockSize, "long"); 172 CachedItem[] small = generateFixedBlocks(15, blockSize / 2, "small"); 173 174 for (CachedItem i : tooLong) { 175 cache.cacheBlock(i.cacheKey, i); 176 } 177 for (CachedItem i : small) { 178 cache.cacheBlock(i.cacheKey, i); 179 } 180 assertEquals(15, cache.getBlockCount()); 181 for (CachedItem i : small) { 182 assertNotNull(cache.getBlock(i.cacheKey, true, false, false)); 183 } 184 for (CachedItem i : tooLong) { 185 assertNull(cache.getBlock(i.cacheKey, true, false, false)); 186 } 187 188 assertEquals(10, cache.getStats().getFailedInserts()); 189 } 190 191 @Test 192 public void testResizeBlockCache() throws Exception { 193 194 long maxSize = 100000; 195 long blockSize = calculateBlockSize(maxSize, 10); 196 197 TinyLfuBlockCache cache = new TinyLfuBlockCache(maxSize, blockSize, blockSize, Runnable::run); 198 199 CachedItem[] blocks = generateFixedBlocks(10, blockSize, "block"); 200 201 for (CachedItem block : blocks) { 202 cache.cacheBlock(block.cacheKey, block); 203 } 204 205 // Do not expect any evictions yet 206 assertEquals(10, cache.getBlockCount()); 207 assertEquals(0, cache.getStats().getEvictionCount()); 208 209 // Resize to half capacity plus an extra block (otherwise we evict an extra) 210 cache.setMaxSize(maxSize / 2); 211 212 // And we expect 1/2 of the blocks to be evicted 213 assertEquals(5, cache.getBlockCount()); 214 assertEquals(5, cache.getStats().getEvictedCount()); 215 } 216 217 private CachedItem[] generateFixedBlocks(int numBlocks, int size, String pfx) { 218 CachedItem[] blocks = new CachedItem[numBlocks]; 219 for (int i = 0; i < numBlocks; i++) { 220 blocks[i] = new CachedItem(pfx + i, size); 221 } 222 return blocks; 223 } 224 225 private CachedItem[] generateFixedBlocks(int numBlocks, long size, String pfx) { 226 return generateFixedBlocks(numBlocks, (int) size, pfx); 227 } 228 229 private CachedItem[] generateRandomBlocks(int numBlocks, long maxSize) { 230 CachedItem[] blocks = new CachedItem[numBlocks]; 231 Random rand = ThreadLocalRandom.current(); 232 for (int i = 0; i < numBlocks; i++) { 233 blocks[i] = new CachedItem("block" + i, rand.nextInt((int) maxSize) + 1); 234 } 235 return blocks; 236 } 237 238 private long calculateBlockSize(long maxSize, int numBlocks) { 239 long roughBlockSize = maxSize / numBlocks; 240 int numEntries = (int) Math.ceil((1.2) * maxSize / roughBlockSize); 241 long totalOverhead = LruBlockCache.CACHE_FIXED_OVERHEAD + ClassSize.CONCURRENT_HASHMAP 242 + (numEntries * ClassSize.CONCURRENT_HASHMAP_ENTRY) 243 + (LruBlockCache.DEFAULT_CONCURRENCY_LEVEL * ClassSize.CONCURRENT_HASHMAP_SEGMENT); 244 long negateBlockSize = totalOverhead / numEntries; 245 negateBlockSize += LruCachedBlock.PER_BLOCK_OVERHEAD; 246 return ClassSize.align((long) Math.floor((roughBlockSize - negateBlockSize) * 0.99f)); 247 } 248 249 private long calculateBlockSizeDefault(long maxSize, int numBlocks) { 250 long roughBlockSize = maxSize / numBlocks; 251 int numEntries = (int) Math.ceil((1.2) * maxSize / roughBlockSize); 252 long totalOverhead = LruBlockCache.CACHE_FIXED_OVERHEAD + ClassSize.CONCURRENT_HASHMAP 253 + (numEntries * ClassSize.CONCURRENT_HASHMAP_ENTRY) 254 + (LruBlockCache.DEFAULT_CONCURRENCY_LEVEL * ClassSize.CONCURRENT_HASHMAP_SEGMENT); 255 long negateBlockSize = totalOverhead / numEntries; 256 negateBlockSize += LruCachedBlock.PER_BLOCK_OVERHEAD; 257 return ClassSize.align((long) Math 258 .floor((roughBlockSize - negateBlockSize) * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR)); 259 } 260 261 private static class CachedItem implements Cacheable { 262 BlockCacheKey cacheKey; 263 int size; 264 265 CachedItem(String blockName, int size) { 266 this.cacheKey = new BlockCacheKey(blockName, 0); 267 this.size = size; 268 } 269 270 /** The size of this item reported to the block cache layer */ 271 @Override 272 public long heapSize() { 273 return ClassSize.align(size); 274 } 275 276 @Override 277 public int getSerializedLength() { 278 return 0; 279 } 280 281 @Override 282 public CacheableDeserializer<Cacheable> getDeserializer() { 283 return null; 284 } 285 286 @Override 287 public BlockType getBlockType() { 288 return BlockType.DATA; 289 } 290 291 @Override 292 public void serialize(ByteBuffer destination, boolean includeNextBlockMetadata) { 293 } 294 } 295 296}