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.apache.hadoop.hbase.io.ByteBuffAllocator.HEAP; 021import static org.junit.Assert.assertEquals; 022import static org.junit.Assert.assertNotNull; 023import static org.junit.Assert.assertNull; 024import static org.junit.Assert.assertTrue; 025 026import java.nio.ByteBuffer; 027import java.util.Random; 028import java.util.concurrent.ExecutorService; 029import java.util.concurrent.Executors; 030import java.util.concurrent.ThreadLocalRandom; 031import java.util.concurrent.TimeUnit; 032import java.util.concurrent.atomic.AtomicBoolean; 033import java.util.concurrent.atomic.AtomicInteger; 034import org.apache.hadoop.conf.Configuration; 035import org.apache.hadoop.hbase.HBaseClassTestRule; 036import org.apache.hadoop.hbase.HBaseConfiguration; 037import org.apache.hadoop.hbase.HConstants; 038import org.apache.hadoop.hbase.Waiter; 039import org.apache.hadoop.hbase.Waiter.ExplainingPredicate; 040import org.apache.hadoop.hbase.io.HeapSize; 041import org.apache.hadoop.hbase.io.hfile.LruBlockCache.EvictionThread; 042import org.apache.hadoop.hbase.nio.ByteBuff; 043import org.apache.hadoop.hbase.testclassification.IOTests; 044import org.apache.hadoop.hbase.testclassification.SmallTests; 045import org.apache.hadoop.hbase.util.ClassSize; 046import org.junit.Assert; 047import org.junit.ClassRule; 048import org.junit.Test; 049import org.junit.experimental.categories.Category; 050import org.slf4j.Logger; 051import org.slf4j.LoggerFactory; 052 053/** 054 * Tests the concurrent LruBlockCache. 055 * <p> 056 * Tests will ensure it grows and shrinks in size properly, evictions run when they're supposed to 057 * and do what they should, and that cached blocks are accessible when expected to be. 058 */ 059@Category({ IOTests.class, SmallTests.class }) 060public class TestLruBlockCache { 061 062 @ClassRule 063 public static final HBaseClassTestRule CLASS_RULE = 064 HBaseClassTestRule.forClass(TestLruBlockCache.class); 065 066 private static final Logger LOG = LoggerFactory.getLogger(TestLruBlockCache.class); 067 068 private static final Configuration CONF = HBaseConfiguration.create(); 069 070 @Test 071 public void testCacheEvictionThreadSafe() throws Exception { 072 long maxSize = 100000; 073 int numBlocks = 9; 074 int testRuns = 10; 075 final long blockSize = calculateBlockSizeDefault(maxSize, numBlocks); 076 assertTrue("calculateBlockSize appears broken.", blockSize * numBlocks <= maxSize); 077 078 final LruBlockCache cache = new LruBlockCache(maxSize, blockSize); 079 EvictionThread evictionThread = cache.getEvictionThread(); 080 assertTrue(evictionThread != null); 081 Waiter.waitFor(CONF, 10000, 100, () -> evictionThread.isEnteringRun()); 082 final String hfileName = "hfile"; 083 int threads = 10; 084 final int blocksPerThread = 5 * numBlocks; 085 for (int run = 0; run != testRuns; ++run) { 086 final AtomicInteger blockCount = new AtomicInteger(0); 087 ExecutorService service = Executors.newFixedThreadPool(threads); 088 for (int i = 0; i != threads; ++i) { 089 service.execute(new Runnable() { 090 @Override 091 public void run() { 092 for (int blockIndex = 0; blockIndex < blocksPerThread 093 || (!cache.isEvictionInProgress()); ++blockIndex) { 094 CachedItem block = 095 new CachedItem(hfileName, (int) blockSize, blockCount.getAndIncrement()); 096 boolean inMemory = Math.random() > 0.5; 097 cache.cacheBlock(block.cacheKey, block, inMemory); 098 } 099 cache.evictBlocksByHfileName(hfileName); 100 } 101 }); 102 } 103 service.shutdown(); 104 // The test may fail here if the evict thread frees the blocks too fast 105 service.awaitTermination(10, TimeUnit.MINUTES); 106 Waiter.waitFor(CONF, 10000, 100, new ExplainingPredicate<Exception>() { 107 @Override 108 public boolean evaluate() throws Exception { 109 return cache.getBlockCount() == 0; 110 } 111 112 @Override 113 public String explainFailure() throws Exception { 114 return "Cache block count failed to return to 0"; 115 } 116 }); 117 assertEquals(0, cache.getBlockCount()); 118 assertEquals(cache.getOverhead(), cache.getCurrentSize()); 119 } 120 } 121 122 @Test 123 public void testBackgroundEvictionThread() throws Exception { 124 long maxSize = 100000; 125 int numBlocks = 9; 126 long blockSize = calculateBlockSizeDefault(maxSize, numBlocks); 127 assertTrue("calculateBlockSize appears broken.", blockSize * numBlocks <= maxSize); 128 129 LruBlockCache cache = new LruBlockCache(maxSize, blockSize); 130 EvictionThread evictionThread = cache.getEvictionThread(); 131 assertTrue(evictionThread != null); 132 133 CachedItem[] blocks = generateFixedBlocks(numBlocks + 1, blockSize, "block"); 134 135 // Make sure eviction thread has entered run method 136 Waiter.waitFor(CONF, 10000, 10, () -> evictionThread.isEnteringRun()); 137 138 // Add all the blocks 139 for (CachedItem block : blocks) { 140 cache.cacheBlock(block.cacheKey, block); 141 } 142 143 // wait until at least one eviction has run 144 Waiter.waitFor(CONF, 30000, 200, new ExplainingPredicate<Exception>() { 145 146 @Override 147 public boolean evaluate() throws Exception { 148 return cache.getStats().getEvictionCount() > 0; 149 } 150 151 @Override 152 public String explainFailure() throws Exception { 153 return "Eviction never happened."; 154 } 155 }); 156 157 // let cache stabilize 158 // On some systems, the cache will run multiple evictions before it attains 159 // steady-state. For instance, after populating the cache with 10 blocks, 160 // the first eviction evicts a single block and then a second eviction 161 // evicts another. I think this is due to the delta between minSize and 162 // acceptableSize, combined with variance between object overhead on 163 // different environments. 164 int n = 0; 165 for (long prevCnt = 0 /* < number of blocks added */, curCnt = cache.getBlockCount(); prevCnt 166 != curCnt; prevCnt = curCnt, curCnt = cache.getBlockCount()) { 167 Thread.sleep(200); 168 assertTrue("Cache never stabilized.", n++ < 100); 169 } 170 171 long evictionCount = cache.getStats().getEvictionCount(); 172 assertTrue(evictionCount >= 1); 173 LOG.info("Background Evictions run: {}", evictionCount); 174 } 175 176 @Test 177 public void testCacheSimple() throws Exception { 178 long maxSize = 1000000; 179 long blockSize = calculateBlockSizeDefault(maxSize, 101); 180 181 LruBlockCache cache = new LruBlockCache(maxSize, blockSize); 182 183 CachedItem[] blocks = generateRandomBlocks(100, blockSize); 184 185 long expectedCacheSize = cache.heapSize(); 186 187 // Confirm empty 188 for (CachedItem block : blocks) { 189 assertTrue(cache.getBlock(block.cacheKey, true, false, true) == null); 190 } 191 192 // Add blocks 193 for (CachedItem block : blocks) { 194 cache.cacheBlock(block.cacheKey, block); 195 expectedCacheSize += block.cacheBlockHeapSize(); 196 } 197 198 // Verify correctly calculated cache heap size 199 assertEquals(expectedCacheSize, cache.heapSize()); 200 201 // Check if all blocks are properly cached and retrieved 202 for (CachedItem block : blocks) { 203 HeapSize buf = cache.getBlock(block.cacheKey, true, false, true); 204 assertTrue(buf != null); 205 assertEquals(buf.heapSize(), block.heapSize()); 206 } 207 208 // Re-add same blocks and ensure nothing has changed 209 long expectedBlockCount = cache.getBlockCount(); 210 for (CachedItem block : blocks) { 211 cache.cacheBlock(block.cacheKey, block); 212 } 213 assertEquals("Cache should ignore cache requests for blocks already in cache", 214 expectedBlockCount, cache.getBlockCount()); 215 216 // Verify correctly calculated cache heap size 217 assertEquals(expectedCacheSize, cache.heapSize()); 218 219 // Check if all blocks are properly cached and retrieved 220 for (CachedItem block : blocks) { 221 HeapSize buf = cache.getBlock(block.cacheKey, true, false, true); 222 assertTrue(buf != null); 223 assertEquals(buf.heapSize(), block.heapSize()); 224 } 225 226 CacheTestUtils.testConvertToJSON(cache); 227 228 // Expect no evictions 229 assertEquals(0, cache.getStats().getEvictionCount()); 230 Thread t = new LruBlockCache.StatisticsThread(cache); 231 t.start(); 232 t.join(); 233 } 234 235 @Test 236 public void testCacheEvictionSimple() throws Exception { 237 long maxSize = 100000; 238 long blockSize = calculateBlockSizeDefault(maxSize, 10); 239 240 LruBlockCache cache = new LruBlockCache(maxSize, blockSize, false); 241 242 CachedItem[] blocks = generateFixedBlocks(10, blockSize, "block"); 243 244 long expectedCacheSize = cache.heapSize(); 245 246 // Add all the blocks 247 for (CachedItem block : blocks) { 248 cache.cacheBlock(block.cacheKey, block); 249 expectedCacheSize += block.cacheBlockHeapSize(); 250 } 251 252 // A single eviction run should have occurred 253 assertEquals(1, cache.getStats().getEvictionCount()); 254 255 // Our expected size overruns acceptable limit 256 assertTrue(expectedCacheSize > (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR)); 257 258 // But the cache did not grow beyond max 259 assertTrue(cache.heapSize() < maxSize); 260 261 // And is still below the acceptable limit 262 assertTrue(cache.heapSize() < (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR)); 263 264 // All blocks except block 0 should be in the cache 265 assertTrue(cache.getBlock(blocks[0].cacheKey, true, false, true) == null); 266 for (int i = 1; i < blocks.length; i++) { 267 assertEquals(cache.getBlock(blocks[i].cacheKey, true, false, true), blocks[i]); 268 } 269 } 270 271 @Test 272 public void testCacheEvictionTwoPriorities() throws Exception { 273 long maxSize = 100000; 274 long blockSize = calculateBlockSizeDefault(maxSize, 10); 275 276 LruBlockCache cache = new LruBlockCache(maxSize, blockSize, false); 277 278 CachedItem[] singleBlocks = generateFixedBlocks(5, 10000, "single"); 279 CachedItem[] multiBlocks = generateFixedBlocks(5, 10000, "multi"); 280 281 long expectedCacheSize = cache.heapSize(); 282 283 // Add and get the multi blocks 284 for (CachedItem block : multiBlocks) { 285 cache.cacheBlock(block.cacheKey, block); 286 expectedCacheSize += block.cacheBlockHeapSize(); 287 assertEquals(cache.getBlock(block.cacheKey, true, false, true), block); 288 } 289 290 // Add the single blocks (no get) 291 for (CachedItem block : singleBlocks) { 292 cache.cacheBlock(block.cacheKey, block); 293 expectedCacheSize += block.heapSize(); 294 } 295 296 // A single eviction run should have occurred 297 assertEquals(1, cache.getStats().getEvictionCount()); 298 299 // We expect two entries evicted 300 assertEquals(2, cache.getStats().getEvictedCount()); 301 302 // Our expected size overruns acceptable limit 303 assertTrue(expectedCacheSize > (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR)); 304 305 // But the cache did not grow beyond max 306 assertTrue(cache.heapSize() <= maxSize); 307 308 // And is now below the acceptable limit 309 assertTrue(cache.heapSize() <= (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR)); 310 311 // We expect fairness across the two priorities. 312 // This test makes multi go barely over its limit, in-memory 313 // empty, and the rest in single. Two single evictions and 314 // one multi eviction expected. 315 assertTrue(cache.getBlock(singleBlocks[0].cacheKey, true, false, true) == null); 316 assertTrue(cache.getBlock(multiBlocks[0].cacheKey, true, false, true) == null); 317 318 // And all others to be cached 319 for (int i = 1; i < 4; i++) { 320 assertEquals(cache.getBlock(singleBlocks[i].cacheKey, true, false, true), singleBlocks[i]); 321 assertEquals(cache.getBlock(multiBlocks[i].cacheKey, true, false, true), multiBlocks[i]); 322 } 323 } 324 325 @Test 326 public void testCacheEvictionThreePriorities() throws Exception { 327 long maxSize = 100000; 328 long blockSize = calculateBlockSize(maxSize, 10); 329 330 LruBlockCache cache = 331 new LruBlockCache(maxSize, blockSize, false, (int) Math.ceil(1.2 * maxSize / blockSize), 332 LruBlockCache.DEFAULT_LOAD_FACTOR, LruBlockCache.DEFAULT_CONCURRENCY_LEVEL, 0.98f, // min 333 0.99f, // acceptable 334 0.33f, // single 335 0.33f, // multi 336 0.34f, // memory 337 1.2f, // limit 338 false, 16 * 1024 * 1024); 339 340 CachedItem[] singleBlocks = generateFixedBlocks(5, blockSize, "single"); 341 CachedItem[] multiBlocks = generateFixedBlocks(5, blockSize, "multi"); 342 CachedItem[] memoryBlocks = generateFixedBlocks(5, blockSize, "memory"); 343 344 long expectedCacheSize = cache.heapSize(); 345 346 // Add 3 blocks from each priority 347 for (int i = 0; i < 3; i++) { 348 349 // Just add single blocks 350 cache.cacheBlock(singleBlocks[i].cacheKey, singleBlocks[i]); 351 expectedCacheSize += singleBlocks[i].cacheBlockHeapSize(); 352 353 // Add and get multi blocks 354 cache.cacheBlock(multiBlocks[i].cacheKey, multiBlocks[i]); 355 expectedCacheSize += multiBlocks[i].cacheBlockHeapSize(); 356 cache.getBlock(multiBlocks[i].cacheKey, true, false, true); 357 358 // Add memory blocks as such 359 cache.cacheBlock(memoryBlocks[i].cacheKey, memoryBlocks[i], true); 360 expectedCacheSize += memoryBlocks[i].cacheBlockHeapSize(); 361 362 } 363 364 // Do not expect any evictions yet 365 assertEquals(0, cache.getStats().getEvictionCount()); 366 367 // Verify cache size 368 assertEquals(expectedCacheSize, cache.heapSize()); 369 370 // Insert a single block, oldest single should be evicted 371 cache.cacheBlock(singleBlocks[3].cacheKey, singleBlocks[3]); 372 373 // Single eviction, one thing evicted 374 assertEquals(1, cache.getStats().getEvictionCount()); 375 assertEquals(1, cache.getStats().getEvictedCount()); 376 377 // Verify oldest single block is the one evicted 378 assertEquals(null, cache.getBlock(singleBlocks[0].cacheKey, true, false, true)); 379 380 // Change the oldest remaining single block to a multi 381 cache.getBlock(singleBlocks[1].cacheKey, true, false, true); 382 383 // Insert another single block 384 cache.cacheBlock(singleBlocks[4].cacheKey, singleBlocks[4]); 385 386 // Two evictions, two evicted. 387 assertEquals(2, cache.getStats().getEvictionCount()); 388 assertEquals(2, cache.getStats().getEvictedCount()); 389 390 // Oldest multi block should be evicted now 391 assertEquals(null, cache.getBlock(multiBlocks[0].cacheKey, true, false, true)); 392 393 // Insert another memory block 394 cache.cacheBlock(memoryBlocks[3].cacheKey, memoryBlocks[3], true); 395 396 // Three evictions, three evicted. 397 assertEquals(3, cache.getStats().getEvictionCount()); 398 assertEquals(3, cache.getStats().getEvictedCount()); 399 400 // Oldest memory block should be evicted now 401 assertEquals(null, cache.getBlock(memoryBlocks[0].cacheKey, true, false, true)); 402 403 // Add a block that is twice as big (should force two evictions) 404 CachedItem[] bigBlocks = generateFixedBlocks(3, blockSize * 3, "big"); 405 cache.cacheBlock(bigBlocks[0].cacheKey, bigBlocks[0]); 406 407 // Four evictions, six evicted (inserted block 3X size, expect +3 evicted) 408 assertEquals(4, cache.getStats().getEvictionCount()); 409 assertEquals(6, cache.getStats().getEvictedCount()); 410 411 // Expect three remaining singles to be evicted 412 assertEquals(null, cache.getBlock(singleBlocks[2].cacheKey, true, false, true)); 413 assertEquals(null, cache.getBlock(singleBlocks[3].cacheKey, true, false, true)); 414 assertEquals(null, cache.getBlock(singleBlocks[4].cacheKey, true, false, true)); 415 416 // Make the big block a multi block 417 cache.getBlock(bigBlocks[0].cacheKey, true, false, true); 418 419 // Cache another single big block 420 cache.cacheBlock(bigBlocks[1].cacheKey, bigBlocks[1]); 421 422 // Five evictions, nine evicted (3 new) 423 assertEquals(5, cache.getStats().getEvictionCount()); 424 assertEquals(9, cache.getStats().getEvictedCount()); 425 426 // Expect three remaining multis to be evicted 427 assertEquals(null, cache.getBlock(singleBlocks[1].cacheKey, true, false, true)); 428 assertEquals(null, cache.getBlock(multiBlocks[1].cacheKey, true, false, true)); 429 assertEquals(null, cache.getBlock(multiBlocks[2].cacheKey, true, false, true)); 430 431 // Cache a big memory block 432 cache.cacheBlock(bigBlocks[2].cacheKey, bigBlocks[2], true); 433 434 // Six evictions, twelve evicted (3 new) 435 assertEquals(6, cache.getStats().getEvictionCount()); 436 assertEquals(12, cache.getStats().getEvictedCount()); 437 438 // Expect three remaining in-memory to be evicted 439 assertEquals(null, cache.getBlock(memoryBlocks[1].cacheKey, true, false, true)); 440 assertEquals(null, cache.getBlock(memoryBlocks[2].cacheKey, true, false, true)); 441 assertEquals(null, cache.getBlock(memoryBlocks[3].cacheKey, true, false, true)); 442 } 443 444 @Test 445 public void testCacheEvictionInMemoryForceMode() throws Exception { 446 long maxSize = 100000; 447 long blockSize = calculateBlockSize(maxSize, 10); 448 449 LruBlockCache cache = 450 new LruBlockCache(maxSize, blockSize, false, (int) Math.ceil(1.2 * maxSize / blockSize), 451 LruBlockCache.DEFAULT_LOAD_FACTOR, LruBlockCache.DEFAULT_CONCURRENCY_LEVEL, 0.98f, // min 452 0.99f, // acceptable 453 0.2f, // single 454 0.3f, // multi 455 0.5f, // memory 456 1.2f, // limit 457 true, 16 * 1024 * 1024); 458 459 CachedItem[] singleBlocks = generateFixedBlocks(10, blockSize, "single"); 460 CachedItem[] multiBlocks = generateFixedBlocks(10, blockSize, "multi"); 461 CachedItem[] memoryBlocks = generateFixedBlocks(10, blockSize, "memory"); 462 463 long expectedCacheSize = cache.heapSize(); 464 465 // 0. Add 5 single blocks and 4 multi blocks to make cache full, si:mu:me = 5:4:0 466 for (int i = 0; i < 4; i++) { 467 // Just add single blocks 468 cache.cacheBlock(singleBlocks[i].cacheKey, singleBlocks[i]); 469 expectedCacheSize += singleBlocks[i].cacheBlockHeapSize(); 470 // Add and get multi blocks 471 cache.cacheBlock(multiBlocks[i].cacheKey, multiBlocks[i]); 472 expectedCacheSize += multiBlocks[i].cacheBlockHeapSize(); 473 cache.getBlock(multiBlocks[i].cacheKey, true, false, true); 474 } 475 // 5th single block 476 cache.cacheBlock(singleBlocks[4].cacheKey, singleBlocks[4]); 477 expectedCacheSize += singleBlocks[4].cacheBlockHeapSize(); 478 // Do not expect any evictions yet 479 assertEquals(0, cache.getStats().getEvictionCount()); 480 // Verify cache size 481 assertEquals(expectedCacheSize, cache.heapSize()); 482 483 // 1. Insert a memory block, oldest single should be evicted, si:mu:me = 4:4:1 484 cache.cacheBlock(memoryBlocks[0].cacheKey, memoryBlocks[0], true); 485 // Single eviction, one block evicted 486 assertEquals(1, cache.getStats().getEvictionCount()); 487 assertEquals(1, cache.getStats().getEvictedCount()); 488 // Verify oldest single block (index = 0) is the one evicted 489 assertEquals(null, cache.getBlock(singleBlocks[0].cacheKey, true, false, true)); 490 491 // 2. Insert another memory block, another single evicted, si:mu:me = 3:4:2 492 cache.cacheBlock(memoryBlocks[1].cacheKey, memoryBlocks[1], true); 493 // Two evictions, two evicted. 494 assertEquals(2, cache.getStats().getEvictionCount()); 495 assertEquals(2, cache.getStats().getEvictedCount()); 496 // Current oldest single block (index = 1) should be evicted now 497 assertEquals(null, cache.getBlock(singleBlocks[1].cacheKey, true, false, true)); 498 499 // 3. Insert 4 memory blocks, 2 single and 2 multi evicted, si:mu:me = 1:2:6 500 cache.cacheBlock(memoryBlocks[2].cacheKey, memoryBlocks[2], true); 501 cache.cacheBlock(memoryBlocks[3].cacheKey, memoryBlocks[3], true); 502 cache.cacheBlock(memoryBlocks[4].cacheKey, memoryBlocks[4], true); 503 cache.cacheBlock(memoryBlocks[5].cacheKey, memoryBlocks[5], true); 504 // Three evictions, three evicted. 505 assertEquals(6, cache.getStats().getEvictionCount()); 506 assertEquals(6, cache.getStats().getEvictedCount()); 507 // two oldest single blocks and two oldest multi blocks evicted 508 assertEquals(null, cache.getBlock(singleBlocks[2].cacheKey, true, false, true)); 509 assertEquals(null, cache.getBlock(singleBlocks[3].cacheKey, true, false, true)); 510 assertEquals(null, cache.getBlock(multiBlocks[0].cacheKey, true, false, true)); 511 assertEquals(null, cache.getBlock(multiBlocks[1].cacheKey, true, false, true)); 512 513 // 4. Insert 3 memory blocks, the remaining 1 single and 2 multi evicted 514 // si:mu:me = 0:0:9 515 cache.cacheBlock(memoryBlocks[6].cacheKey, memoryBlocks[6], true); 516 cache.cacheBlock(memoryBlocks[7].cacheKey, memoryBlocks[7], true); 517 cache.cacheBlock(memoryBlocks[8].cacheKey, memoryBlocks[8], true); 518 // Three evictions, three evicted. 519 assertEquals(9, cache.getStats().getEvictionCount()); 520 assertEquals(9, cache.getStats().getEvictedCount()); 521 // one oldest single block and two oldest multi blocks evicted 522 assertEquals(null, cache.getBlock(singleBlocks[4].cacheKey, true, false, true)); 523 assertEquals(null, cache.getBlock(multiBlocks[2].cacheKey, true, false, true)); 524 assertEquals(null, cache.getBlock(multiBlocks[3].cacheKey, true, false, true)); 525 526 // 5. Insert one memory block, the oldest memory evicted 527 // si:mu:me = 0:0:9 528 cache.cacheBlock(memoryBlocks[9].cacheKey, memoryBlocks[9], true); 529 // one eviction, one evicted. 530 assertEquals(10, cache.getStats().getEvictionCount()); 531 assertEquals(10, cache.getStats().getEvictedCount()); 532 // oldest memory block evicted 533 assertEquals(null, cache.getBlock(memoryBlocks[0].cacheKey, true, false, true)); 534 535 // 6. Insert one new single block, itself evicted immediately since 536 // all blocks in cache are memory-type which have higher priority 537 // si:mu:me = 0:0:9 (no change) 538 cache.cacheBlock(singleBlocks[9].cacheKey, singleBlocks[9]); 539 // one eviction, one evicted. 540 assertEquals(11, cache.getStats().getEvictionCount()); 541 assertEquals(11, cache.getStats().getEvictedCount()); 542 // the single block just cached now evicted (can't evict memory) 543 assertEquals(null, cache.getBlock(singleBlocks[9].cacheKey, true, false, true)); 544 } 545 546 // test scan resistance 547 @Test 548 public void testScanResistance() throws Exception { 549 550 long maxSize = 100000; 551 long blockSize = calculateBlockSize(maxSize, 10); 552 553 LruBlockCache cache = 554 new LruBlockCache(maxSize, blockSize, false, (int) Math.ceil(1.2 * maxSize / blockSize), 555 LruBlockCache.DEFAULT_LOAD_FACTOR, LruBlockCache.DEFAULT_CONCURRENCY_LEVEL, 0.66f, // min 556 0.99f, // acceptable 557 0.33f, // single 558 0.33f, // multi 559 0.34f, // memory 560 1.2f, // limit 561 false, 16 * 1024 * 1024); 562 563 CachedItem[] singleBlocks = generateFixedBlocks(20, blockSize, "single"); 564 CachedItem[] multiBlocks = generateFixedBlocks(5, blockSize, "multi"); 565 566 // Add 5 multi blocks 567 for (CachedItem block : multiBlocks) { 568 cache.cacheBlock(block.cacheKey, block); 569 cache.getBlock(block.cacheKey, true, false, true); 570 } 571 572 // Add 5 single blocks 573 for (int i = 0; i < 5; i++) { 574 cache.cacheBlock(singleBlocks[i].cacheKey, singleBlocks[i]); 575 } 576 577 // An eviction ran 578 assertEquals(1, cache.getStats().getEvictionCount()); 579 580 // To drop down to 2/3 capacity, we'll need to evict 4 blocks 581 assertEquals(4, cache.getStats().getEvictedCount()); 582 583 // Should have been taken off equally from single and multi 584 assertEquals(null, cache.getBlock(singleBlocks[0].cacheKey, true, false, true)); 585 assertEquals(null, cache.getBlock(singleBlocks[1].cacheKey, true, false, true)); 586 assertEquals(null, cache.getBlock(multiBlocks[0].cacheKey, true, false, true)); 587 assertEquals(null, cache.getBlock(multiBlocks[1].cacheKey, true, false, true)); 588 589 // Let's keep "scanning" by adding single blocks. From here on we only 590 // expect evictions from the single bucket. 591 592 // Every time we reach 10 total blocks (every 4 inserts) we get 4 single 593 // blocks evicted. Inserting 13 blocks should yield 3 more evictions and 594 // 12 more evicted. 595 596 for (int i = 5; i < 18; i++) { 597 cache.cacheBlock(singleBlocks[i].cacheKey, singleBlocks[i]); 598 } 599 600 // 4 total evictions, 16 total evicted 601 assertEquals(4, cache.getStats().getEvictionCount()); 602 assertEquals(16, cache.getStats().getEvictedCount()); 603 604 // Should now have 7 total blocks 605 assertEquals(7, cache.getBlockCount()); 606 607 } 608 609 @Test 610 public void testMaxBlockSize() throws Exception { 611 long maxSize = 100000; 612 long blockSize = calculateBlockSize(maxSize, 10); 613 614 LruBlockCache cache = 615 new LruBlockCache(maxSize, blockSize, false, (int) Math.ceil(1.2 * maxSize / blockSize), 616 LruBlockCache.DEFAULT_LOAD_FACTOR, LruBlockCache.DEFAULT_CONCURRENCY_LEVEL, 0.66f, // min 617 0.99f, // acceptable 618 0.33f, // single 619 0.33f, // multi 620 0.34f, // memory 621 1.2f, // limit 622 false, 1024); 623 CachedItem[] tooLong = generateFixedBlocks(10, 1024 + 5, "long"); 624 CachedItem[] small = generateFixedBlocks(15, 600, "small"); 625 626 for (CachedItem i : tooLong) { 627 cache.cacheBlock(i.cacheKey, i); 628 } 629 for (CachedItem i : small) { 630 cache.cacheBlock(i.cacheKey, i); 631 } 632 assertEquals(15, cache.getBlockCount()); 633 for (CachedItem i : small) { 634 assertNotNull(cache.getBlock(i.cacheKey, true, false, false)); 635 } 636 for (CachedItem i : tooLong) { 637 assertNull(cache.getBlock(i.cacheKey, true, false, false)); 638 } 639 640 assertEquals(10, cache.getStats().getFailedInserts()); 641 } 642 643 // test setMaxSize 644 @Test 645 public void testResizeBlockCache() throws Exception { 646 long maxSize = 300000; 647 long blockSize = calculateBlockSize(maxSize, 31); 648 649 LruBlockCache cache = 650 new LruBlockCache(maxSize, blockSize, false, (int) Math.ceil(1.2 * maxSize / blockSize), 651 LruBlockCache.DEFAULT_LOAD_FACTOR, LruBlockCache.DEFAULT_CONCURRENCY_LEVEL, 0.98f, // min 652 0.99f, // acceptable 653 0.33f, // single 654 0.33f, // multi 655 0.34f, // memory 656 1.2f, // limit 657 false, 16 * 1024 * 1024); 658 659 CachedItem[] singleBlocks = generateFixedBlocks(10, blockSize, "single"); 660 CachedItem[] multiBlocks = generateFixedBlocks(10, blockSize, "multi"); 661 CachedItem[] memoryBlocks = generateFixedBlocks(10, blockSize, "memory"); 662 663 // Add all blocks from all priorities 664 for (int i = 0; i < 10; i++) { 665 // Just add single blocks 666 cache.cacheBlock(singleBlocks[i].cacheKey, singleBlocks[i]); 667 668 // Add and get multi blocks 669 cache.cacheBlock(multiBlocks[i].cacheKey, multiBlocks[i]); 670 cache.getBlock(multiBlocks[i].cacheKey, true, false, true); 671 672 // Add memory blocks as such 673 cache.cacheBlock(memoryBlocks[i].cacheKey, memoryBlocks[i], true); 674 } 675 676 // Do not expect any evictions yet 677 assertEquals(0, cache.getStats().getEvictionCount()); 678 679 // Resize to half capacity plus an extra block (otherwise we evict an extra) 680 cache.setMaxSize((long) (maxSize * 0.5f)); 681 682 // Should have run a single eviction 683 assertEquals(1, cache.getStats().getEvictionCount()); 684 685 // And we expect 1/2 of the blocks to be evicted 686 assertEquals(15, cache.getStats().getEvictedCount()); 687 688 // And the oldest 5 blocks from each category should be gone 689 for (int i = 0; i < 5; i++) { 690 assertEquals(null, cache.getBlock(singleBlocks[i].cacheKey, true, false, true)); 691 assertEquals(null, cache.getBlock(multiBlocks[i].cacheKey, true, false, true)); 692 assertEquals(null, cache.getBlock(memoryBlocks[i].cacheKey, true, false, true)); 693 } 694 695 // And the newest 5 blocks should still be accessible 696 for (int i = 5; i < 10; i++) { 697 assertEquals(singleBlocks[i], cache.getBlock(singleBlocks[i].cacheKey, true, false, true)); 698 assertEquals(multiBlocks[i], cache.getBlock(multiBlocks[i].cacheKey, true, false, true)); 699 assertEquals(memoryBlocks[i], cache.getBlock(memoryBlocks[i].cacheKey, true, false, true)); 700 } 701 } 702 703 // test metricsPastNPeriods 704 @Test 705 public void testPastNPeriodsMetrics() throws Exception { 706 double delta = 0.01; 707 708 // 3 total periods 709 CacheStats stats = new CacheStats("test", 3); 710 711 // No accesses, should be 0 712 stats.rollMetricsPeriod(); 713 assertEquals(0.0, stats.getHitRatioPastNPeriods(), delta); 714 assertEquals(0.0, stats.getHitCachingRatioPastNPeriods(), delta); 715 716 // period 1, 1 hit caching, 1 hit non-caching, 2 miss non-caching 717 // should be (2/4)=0.5 and (1/1)=1 718 stats.hit(false, true, BlockType.DATA); 719 stats.hit(true, true, BlockType.DATA); 720 stats.miss(false, false, BlockType.DATA); 721 stats.miss(false, false, BlockType.DATA); 722 stats.rollMetricsPeriod(); 723 assertEquals(0.5, stats.getHitRatioPastNPeriods(), delta); 724 assertEquals(1.0, stats.getHitCachingRatioPastNPeriods(), delta); 725 726 // period 2, 1 miss caching, 3 miss non-caching 727 // should be (2/8)=0.25 and (1/2)=0.5 728 stats.miss(true, false, BlockType.DATA); 729 stats.miss(false, false, BlockType.DATA); 730 stats.miss(false, false, BlockType.DATA); 731 stats.miss(false, false, BlockType.DATA); 732 stats.rollMetricsPeriod(); 733 assertEquals(0.25, stats.getHitRatioPastNPeriods(), delta); 734 assertEquals(0.5, stats.getHitCachingRatioPastNPeriods(), delta); 735 736 // period 3, 2 hits of each type 737 // should be (6/12)=0.5 and (3/4)=0.75 738 stats.hit(false, true, BlockType.DATA); 739 stats.hit(true, true, BlockType.DATA); 740 stats.hit(false, true, BlockType.DATA); 741 stats.hit(true, true, BlockType.DATA); 742 stats.rollMetricsPeriod(); 743 assertEquals(0.5, stats.getHitRatioPastNPeriods(), delta); 744 assertEquals(0.75, stats.getHitCachingRatioPastNPeriods(), delta); 745 746 // period 4, evict period 1, two caching misses 747 // should be (4/10)=0.4 and (2/5)=0.4 748 stats.miss(true, false, BlockType.DATA); 749 stats.miss(true, false, BlockType.DATA); 750 stats.rollMetricsPeriod(); 751 assertEquals(0.4, stats.getHitRatioPastNPeriods(), delta); 752 assertEquals(0.4, stats.getHitCachingRatioPastNPeriods(), delta); 753 754 // period 5, evict period 2, 2 caching misses, 2 non-caching hit 755 // should be (6/10)=0.6 and (2/6)=1/3 756 stats.miss(true, false, BlockType.DATA); 757 stats.miss(true, false, BlockType.DATA); 758 stats.hit(false, true, BlockType.DATA); 759 stats.hit(false, true, BlockType.DATA); 760 stats.rollMetricsPeriod(); 761 assertEquals(0.6, stats.getHitRatioPastNPeriods(), delta); 762 assertEquals((double) 1 / 3, stats.getHitCachingRatioPastNPeriods(), delta); 763 764 // period 6, evict period 3 765 // should be (2/6)=1/3 and (0/4)=0 766 stats.rollMetricsPeriod(); 767 assertEquals((double) 1 / 3, stats.getHitRatioPastNPeriods(), delta); 768 assertEquals(0.0, stats.getHitCachingRatioPastNPeriods(), delta); 769 770 // period 7, evict period 4 771 // should be (2/4)=0.5 and (0/2)=0 772 stats.rollMetricsPeriod(); 773 assertEquals(0.5, stats.getHitRatioPastNPeriods(), delta); 774 assertEquals(0.0, stats.getHitCachingRatioPastNPeriods(), delta); 775 776 // period 8, evict period 5 777 // should be 0 and 0 778 stats.rollMetricsPeriod(); 779 assertEquals(0.0, stats.getHitRatioPastNPeriods(), delta); 780 assertEquals(0.0, stats.getHitCachingRatioPastNPeriods(), delta); 781 782 // period 9, one of each 783 // should be (2/4)=0.5 and (1/2)=0.5 784 stats.miss(true, false, BlockType.DATA); 785 stats.miss(false, false, BlockType.DATA); 786 stats.hit(true, true, BlockType.DATA); 787 stats.hit(false, true, BlockType.DATA); 788 stats.rollMetricsPeriod(); 789 assertEquals(0.5, stats.getHitRatioPastNPeriods(), delta); 790 assertEquals(0.5, stats.getHitCachingRatioPastNPeriods(), delta); 791 } 792 793 @Test 794 public void testCacheBlockNextBlockMetadataMissing() { 795 long maxSize = 100000; 796 long blockSize = calculateBlockSize(maxSize, 10); 797 int size = 100; 798 int length = HConstants.HFILEBLOCK_HEADER_SIZE + size; 799 byte[] byteArr = new byte[length]; 800 ByteBuffer buf = ByteBuffer.wrap(byteArr, 0, size); 801 HFileContext meta = new HFileContextBuilder().build(); 802 HFileBlock blockWithNextBlockMetadata = new HFileBlock(BlockType.DATA, size, size, -1, 803 ByteBuff.wrap(buf), HFileBlock.FILL_HEADER, -1, 52, -1, meta, HEAP); 804 HFileBlock blockWithoutNextBlockMetadata = new HFileBlock(BlockType.DATA, size, size, -1, 805 ByteBuff.wrap(buf), HFileBlock.FILL_HEADER, -1, -1, -1, meta, HEAP); 806 807 LruBlockCache cache = 808 new LruBlockCache(maxSize, blockSize, false, (int) Math.ceil(1.2 * maxSize / blockSize), 809 LruBlockCache.DEFAULT_LOAD_FACTOR, LruBlockCache.DEFAULT_CONCURRENCY_LEVEL, 0.66f, // min 810 0.99f, // acceptable 811 0.33f, // single 812 0.33f, // multi 813 0.34f, // memory 814 1.2f, // limit 815 false, 1024); 816 817 BlockCacheKey key = new BlockCacheKey("key1", 0); 818 ByteBuffer actualBuffer = ByteBuffer.allocate(length); 819 ByteBuffer block1Buffer = ByteBuffer.allocate(length); 820 ByteBuffer block2Buffer = ByteBuffer.allocate(length); 821 blockWithNextBlockMetadata.serialize(block1Buffer, true); 822 blockWithoutNextBlockMetadata.serialize(block2Buffer, true); 823 824 // Add blockWithNextBlockMetadata, expect blockWithNextBlockMetadata back. 825 CacheTestUtils.getBlockAndAssertEquals(cache, key, blockWithNextBlockMetadata, actualBuffer, 826 block1Buffer); 827 828 // Add blockWithoutNextBlockMetada, expect blockWithNextBlockMetadata back. 829 CacheTestUtils.getBlockAndAssertEquals(cache, key, blockWithoutNextBlockMetadata, actualBuffer, 830 block1Buffer); 831 832 // Clear and add blockWithoutNextBlockMetadata 833 cache.clearCache(); 834 assertNull(cache.getBlock(key, false, false, false)); 835 CacheTestUtils.getBlockAndAssertEquals(cache, key, blockWithoutNextBlockMetadata, actualBuffer, 836 block2Buffer); 837 838 // Add blockWithNextBlockMetadata, expect blockWithNextBlockMetadata to replace. 839 CacheTestUtils.getBlockAndAssertEquals(cache, key, blockWithNextBlockMetadata, actualBuffer, 840 block1Buffer); 841 } 842 843 private CachedItem[] generateFixedBlocks(int numBlocks, int size, String pfx) { 844 CachedItem[] blocks = new CachedItem[numBlocks]; 845 for (int i = 0; i < numBlocks; i++) { 846 blocks[i] = new CachedItem(pfx + i, size); 847 } 848 return blocks; 849 } 850 851 private CachedItem[] generateFixedBlocks(int numBlocks, long size, String pfx) { 852 return generateFixedBlocks(numBlocks, (int) size, pfx); 853 } 854 855 private CachedItem[] generateRandomBlocks(int numBlocks, long maxSize) { 856 CachedItem[] blocks = new CachedItem[numBlocks]; 857 Random rand = ThreadLocalRandom.current(); 858 for (int i = 0; i < numBlocks; i++) { 859 blocks[i] = new CachedItem("block" + i, rand.nextInt((int) maxSize) + 1); 860 } 861 return blocks; 862 } 863 864 private long calculateBlockSize(long maxSize, int numBlocks) { 865 long roughBlockSize = maxSize / numBlocks; 866 int numEntries = (int) Math.ceil((1.2) * maxSize / roughBlockSize); 867 long totalOverhead = LruBlockCache.CACHE_FIXED_OVERHEAD + ClassSize.CONCURRENT_HASHMAP 868 + (numEntries * ClassSize.CONCURRENT_HASHMAP_ENTRY) 869 + (LruBlockCache.DEFAULT_CONCURRENCY_LEVEL * ClassSize.CONCURRENT_HASHMAP_SEGMENT); 870 long negateBlockSize = (long) (totalOverhead / numEntries); 871 negateBlockSize += LruCachedBlock.PER_BLOCK_OVERHEAD; 872 return ClassSize.align((long) Math.floor((roughBlockSize - negateBlockSize) * 0.99f)); 873 } 874 875 private long calculateBlockSizeDefault(long maxSize, int numBlocks) { 876 long roughBlockSize = maxSize / numBlocks; 877 int numEntries = (int) Math.ceil((1.2) * maxSize / roughBlockSize); 878 long totalOverhead = LruBlockCache.CACHE_FIXED_OVERHEAD + ClassSize.CONCURRENT_HASHMAP 879 + (numEntries * ClassSize.CONCURRENT_HASHMAP_ENTRY) 880 + (LruBlockCache.DEFAULT_CONCURRENCY_LEVEL * ClassSize.CONCURRENT_HASHMAP_SEGMENT); 881 long negateBlockSize = totalOverhead / numEntries; 882 negateBlockSize += LruCachedBlock.PER_BLOCK_OVERHEAD; 883 return ClassSize.align((long) Math 884 .floor((roughBlockSize - negateBlockSize) * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR)); 885 } 886 887 private static class CachedItem implements Cacheable { 888 BlockCacheKey cacheKey; 889 int size; 890 891 CachedItem(String blockName, int size, int offset) { 892 this.cacheKey = new BlockCacheKey(blockName, offset); 893 this.size = size; 894 } 895 896 CachedItem(String blockName, int size) { 897 this.cacheKey = new BlockCacheKey(blockName, 0); 898 this.size = size; 899 } 900 901 /** The size of this item reported to the block cache layer */ 902 @Override 903 public long heapSize() { 904 return ClassSize.align(size); 905 } 906 907 /** Size of the cache block holding this item. Used for verification. */ 908 public long cacheBlockHeapSize() { 909 return LruCachedBlock.PER_BLOCK_OVERHEAD + ClassSize.align(cacheKey.heapSize()) 910 + ClassSize.align(size); 911 } 912 913 @Override 914 public int getSerializedLength() { 915 return 0; 916 } 917 918 @Override 919 public CacheableDeserializer<Cacheable> getDeserializer() { 920 return null; 921 } 922 923 @Override 924 public void serialize(ByteBuffer destination, boolean includeNextBlockMetadata) { 925 } 926 927 @Override 928 public BlockType getBlockType() { 929 return BlockType.DATA; 930 } 931 } 932 933 static void testMultiThreadGetAndEvictBlockInternal(BlockCache cache) throws Exception { 934 int size = 100; 935 int length = HConstants.HFILEBLOCK_HEADER_SIZE + size; 936 byte[] byteArr = new byte[length]; 937 HFileContext meta = new HFileContextBuilder().build(); 938 BlockCacheKey key = new BlockCacheKey("key1", 0); 939 HFileBlock blk = new HFileBlock(BlockType.DATA, size, size, -1, 940 ByteBuff.wrap(ByteBuffer.wrap(byteArr, 0, size)), HFileBlock.FILL_HEADER, -1, 52, -1, meta, 941 HEAP); 942 AtomicBoolean err1 = new AtomicBoolean(false); 943 Thread t1 = new Thread(() -> { 944 for (int i = 0; i < 10000 && !err1.get(); i++) { 945 try { 946 cache.getBlock(key, false, false, true); 947 } catch (Exception e) { 948 err1.set(true); 949 LOG.info("Cache block or get block failure: ", e); 950 } 951 } 952 }); 953 954 AtomicBoolean err2 = new AtomicBoolean(false); 955 Thread t2 = new Thread(() -> { 956 for (int i = 0; i < 10000 && !err2.get(); i++) { 957 try { 958 cache.evictBlock(key); 959 } catch (Exception e) { 960 err2.set(true); 961 LOG.info("Evict block failure: ", e); 962 } 963 } 964 }); 965 966 AtomicBoolean err3 = new AtomicBoolean(false); 967 Thread t3 = new Thread(() -> { 968 for (int i = 0; i < 10000 && !err3.get(); i++) { 969 try { 970 cache.cacheBlock(key, blk); 971 } catch (Exception e) { 972 err3.set(true); 973 LOG.info("Cache block failure: ", e); 974 } 975 } 976 }); 977 t1.start(); 978 t2.start(); 979 t3.start(); 980 t1.join(); 981 t2.join(); 982 t3.join(); 983 Assert.assertFalse(err1.get()); 984 Assert.assertFalse(err2.get()); 985 Assert.assertFalse(err3.get()); 986 } 987 988 @Test 989 public void testMultiThreadGetAndEvictBlock() throws Exception { 990 long maxSize = 100000; 991 long blockSize = calculateBlockSize(maxSize, 10); 992 LruBlockCache cache = 993 new LruBlockCache(maxSize, blockSize, false, (int) Math.ceil(1.2 * maxSize / blockSize), 994 LruBlockCache.DEFAULT_LOAD_FACTOR, LruBlockCache.DEFAULT_CONCURRENCY_LEVEL, 0.66f, // min 995 0.99f, // acceptable 996 0.33f, // single 997 0.33f, // multi 998 0.34f, // memory 999 1.2f, // limit 1000 false, 1024); 1001 testMultiThreadGetAndEvictBlockInternal(cache); 1002 } 1003}