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