1   /**
2    * Copyright 2009 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  package org.apache.hadoop.hbase.io.hfile;
21  
22  import java.nio.ByteBuffer;
23  import java.util.Random;
24  
25  import org.apache.hadoop.hbase.io.HeapSize;
26  import org.apache.hadoop.hbase.util.ClassSize;
27  
28  import junit.framework.TestCase;
29  
30  /**
31   * Tests the concurrent LruBlockCache.<p>
32   *
33   * Tests will ensure it grows and shrinks in size properly,
34   * evictions run when they're supposed to and do what they should,
35   * and that cached blocks are accessible when expected to be.
36   */
37  public class TestLruBlockCache extends TestCase {
38  
39    public void testBackgroundEvictionThread() throws Exception {
40  
41      long maxSize = 100000;
42      long blockSize = calculateBlockSizeDefault(maxSize, 9); // room for 9, will evict
43  
44      LruBlockCache cache = new LruBlockCache(maxSize,blockSize);
45  
46      CachedItem [] blocks = generateFixedBlocks(10, blockSize, "block");
47  
48      // Add all the blocks
49      for (CachedItem block : blocks) {
50        cache.cacheBlock(block.blockName, block);
51      }
52  
53      // Let the eviction run
54      int n = 0;
55      while(cache.getEvictionCount() == 0) {
56        Thread.sleep(200);
57        assertTrue(n++ < 10);
58      }
59      System.out.println("Background Evictions run: " + cache.getEvictionCount());
60  
61      // A single eviction run should have occurred
62      assertEquals(cache.getEvictionCount(), 1);
63    }
64  
65    public void testCacheSimple() throws Exception {
66  
67      long maxSize = 1000000;
68      long blockSize = calculateBlockSizeDefault(maxSize, 101);
69  
70      LruBlockCache cache = new LruBlockCache(maxSize, blockSize);
71  
72      CachedItem [] blocks = generateRandomBlocks(100, blockSize);
73  
74      long expectedCacheSize = cache.heapSize();
75  
76      // Confirm empty
77      for (CachedItem block : blocks) {
78        assertTrue(cache.getBlock(block.blockName, true) == null);
79      }
80  
81      // Add blocks
82      for (CachedItem block : blocks) {
83        cache.cacheBlock(block.blockName, block);
84        expectedCacheSize += block.cacheBlockHeapSize();
85      }
86  
87      // Verify correctly calculated cache heap size
88      assertEquals(expectedCacheSize, cache.heapSize());
89  
90      // Check if all blocks are properly cached and retrieved
91      for (CachedItem block : blocks) {
92        HeapSize buf = cache.getBlock(block.blockName, true);
93        assertTrue(buf != null);
94        assertEquals(buf.heapSize(), block.heapSize());
95      }
96  
97      // Re-add same blocks and ensure nothing has changed
98      for (CachedItem block : blocks) {
99        try {
100         cache.cacheBlock(block.blockName, block);
101         assertTrue("Cache should not allow re-caching a block", false);
102       } catch(RuntimeException re) {
103         // expected
104       }
105     }
106 
107     // Verify correctly calculated cache heap size
108     assertEquals(expectedCacheSize, cache.heapSize());
109 
110     // Check if all blocks are properly cached and retrieved
111     for (CachedItem block : blocks) {
112       HeapSize buf = cache.getBlock(block.blockName, true);
113       assertTrue(buf != null);
114       assertEquals(buf.heapSize(), block.heapSize());
115     }
116 
117     // Expect no evictions
118     assertEquals(0, cache.getEvictionCount());
119     Thread t = new LruBlockCache.StatisticsThread(cache);
120     t.start();
121     t.join();
122   }
123 
124   public void testCacheEvictionSimple() throws Exception {
125 
126     long maxSize = 100000;
127     long blockSize = calculateBlockSizeDefault(maxSize, 10);
128 
129     LruBlockCache cache = new LruBlockCache(maxSize,blockSize,false);
130 
131     CachedItem [] blocks = generateFixedBlocks(10, blockSize, "block");
132 
133     long expectedCacheSize = cache.heapSize();
134 
135     // Add all the blocks
136     for (CachedItem block : blocks) {
137       cache.cacheBlock(block.blockName, block);
138       expectedCacheSize += block.cacheBlockHeapSize();
139     }
140 
141     // A single eviction run should have occurred
142     assertEquals(1, cache.getEvictionCount());
143 
144     // Our expected size overruns acceptable limit
145     assertTrue(expectedCacheSize >
146       (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR));
147 
148     // But the cache did not grow beyond max
149     assertTrue(cache.heapSize() < maxSize);
150 
151     // And is still below the acceptable limit
152     assertTrue(cache.heapSize() <
153         (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR));
154 
155     // All blocks except block 0 and 1 should be in the cache
156     assertTrue(cache.getBlock(blocks[0].blockName, true) == null);
157     assertTrue(cache.getBlock(blocks[1].blockName, true) == null);
158     for(int i=2;i<blocks.length;i++) {
159       assertEquals(cache.getBlock(blocks[i].blockName, true),
160           blocks[i]);
161     }
162   }
163 
164   public void testCacheEvictionTwoPriorities() throws Exception {
165 
166     long maxSize = 100000;
167     long blockSize = calculateBlockSizeDefault(maxSize, 10);
168 
169     LruBlockCache cache = new LruBlockCache(maxSize,blockSize,false);
170 
171     CachedItem [] singleBlocks = generateFixedBlocks(5, 10000, "single");
172     CachedItem [] multiBlocks = generateFixedBlocks(5, 10000, "multi");
173 
174     long expectedCacheSize = cache.heapSize();
175 
176     // Add and get the multi blocks
177     for (CachedItem block : multiBlocks) {
178       cache.cacheBlock(block.blockName, block);
179       expectedCacheSize += block.cacheBlockHeapSize();
180       assertEquals(cache.getBlock(block.blockName, true), block);
181     }
182 
183     // Add the single blocks (no get)
184     for (CachedItem block : singleBlocks) {
185       cache.cacheBlock(block.blockName, block);
186       expectedCacheSize += block.heapSize();
187     }
188 
189     // A single eviction run should have occurred
190     assertEquals(cache.getEvictionCount(), 1);
191 
192     // We expect two entries evicted
193     assertEquals(cache.getEvictedCount(), 2);
194 
195     // Our expected size overruns acceptable limit
196     assertTrue(expectedCacheSize >
197       (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR));
198 
199     // But the cache did not grow beyond max
200     assertTrue(cache.heapSize() <= maxSize);
201 
202     // And is now below the acceptable limit
203     assertTrue(cache.heapSize() <=
204         (maxSize * LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR));
205 
206     // We expect fairness across the two priorities.
207     // This test makes multi go barely over its limit, in-memory
208     // empty, and the rest in single.  Two single evictions and
209     // one multi eviction expected.
210     assertTrue(cache.getBlock(singleBlocks[0].blockName, true) == null);
211     assertTrue(cache.getBlock(multiBlocks[0].blockName, true) == null);
212 
213     // And all others to be cached
214     for(int i=1;i<4;i++) {
215       assertEquals(cache.getBlock(singleBlocks[i].blockName, true),
216           singleBlocks[i]);
217       assertEquals(cache.getBlock(multiBlocks[i].blockName, true),
218           multiBlocks[i]);
219     }
220   }
221 
222   public void testCacheEvictionThreePriorities() throws Exception {
223 
224     long maxSize = 100000;
225     long blockSize = calculateBlockSize(maxSize, 10);
226 
227     LruBlockCache cache = new LruBlockCache(maxSize, blockSize, false,
228         (int)Math.ceil(1.2*maxSize/blockSize),
229         LruBlockCache.DEFAULT_LOAD_FACTOR,
230         LruBlockCache.DEFAULT_CONCURRENCY_LEVEL,
231         0.98f, // min
232         0.99f, // acceptable
233         0.33f, // single
234         0.33f, // multi
235         0.34f);// memory
236 
237 
238     CachedItem [] singleBlocks = generateFixedBlocks(5, blockSize, "single");
239     CachedItem [] multiBlocks = generateFixedBlocks(5, blockSize, "multi");
240     CachedItem [] memoryBlocks = generateFixedBlocks(5, blockSize, "memory");
241 
242     long expectedCacheSize = cache.heapSize();
243 
244     // Add 3 blocks from each priority
245     for(int i=0;i<3;i++) {
246 
247       // Just add single blocks
248       cache.cacheBlock(singleBlocks[i].blockName, singleBlocks[i]);
249       expectedCacheSize += singleBlocks[i].cacheBlockHeapSize();
250 
251       // Add and get multi blocks
252       cache.cacheBlock(multiBlocks[i].blockName, multiBlocks[i]);
253       expectedCacheSize += multiBlocks[i].cacheBlockHeapSize();
254       cache.getBlock(multiBlocks[i].blockName, true);
255 
256       // Add memory blocks as such
257       cache.cacheBlock(memoryBlocks[i].blockName, memoryBlocks[i], true);
258       expectedCacheSize += memoryBlocks[i].cacheBlockHeapSize();
259 
260     }
261 
262     // Do not expect any evictions yet
263     assertEquals(0, cache.getEvictionCount());
264 
265     // Verify cache size
266     assertEquals(expectedCacheSize, cache.heapSize());
267 
268     // Insert a single block, oldest single should be evicted
269     cache.cacheBlock(singleBlocks[3].blockName, singleBlocks[3]);
270 
271     // Single eviction, one thing evicted
272     assertEquals(1, cache.getEvictionCount());
273     assertEquals(1, cache.getEvictedCount());
274 
275     // Verify oldest single block is the one evicted
276     assertEquals(null, cache.getBlock(singleBlocks[0].blockName, true));
277 
278     // Change the oldest remaining single block to a multi
279     cache.getBlock(singleBlocks[1].blockName, true);
280 
281     // Insert another single block
282     cache.cacheBlock(singleBlocks[4].blockName, singleBlocks[4]);
283 
284     // Two evictions, two evicted.
285     assertEquals(2, cache.getEvictionCount());
286     assertEquals(2, cache.getEvictedCount());
287 
288     // Oldest multi block should be evicted now
289     assertEquals(null, cache.getBlock(multiBlocks[0].blockName, true));
290 
291     // Insert another memory block
292     cache.cacheBlock(memoryBlocks[3].blockName, memoryBlocks[3], true);
293 
294     // Three evictions, three evicted.
295     assertEquals(3, cache.getEvictionCount());
296     assertEquals(3, cache.getEvictedCount());
297 
298     // Oldest memory block should be evicted now
299     assertEquals(null, cache.getBlock(memoryBlocks[0].blockName, true));
300 
301     // Add a block that is twice as big (should force two evictions)
302     CachedItem [] bigBlocks = generateFixedBlocks(3, blockSize*3, "big");
303     cache.cacheBlock(bigBlocks[0].blockName, bigBlocks[0]);
304 
305     // Four evictions, six evicted (inserted block 3X size, expect +3 evicted)
306     assertEquals(4, cache.getEvictionCount());
307     assertEquals(6, cache.getEvictedCount());
308 
309     // Expect three remaining singles to be evicted
310     assertEquals(null, cache.getBlock(singleBlocks[2].blockName, true));
311     assertEquals(null, cache.getBlock(singleBlocks[3].blockName, true));
312     assertEquals(null, cache.getBlock(singleBlocks[4].blockName, true));
313 
314     // Make the big block a multi block
315     cache.getBlock(bigBlocks[0].blockName, true);
316 
317     // Cache another single big block
318     cache.cacheBlock(bigBlocks[1].blockName, bigBlocks[1]);
319 
320     // Five evictions, nine evicted (3 new)
321     assertEquals(5, cache.getEvictionCount());
322     assertEquals(9, cache.getEvictedCount());
323 
324     // Expect three remaining multis to be evicted
325     assertEquals(null, cache.getBlock(singleBlocks[1].blockName, true));
326     assertEquals(null, cache.getBlock(multiBlocks[1].blockName, true));
327     assertEquals(null, cache.getBlock(multiBlocks[2].blockName, true));
328 
329     // Cache a big memory block
330     cache.cacheBlock(bigBlocks[2].blockName, bigBlocks[2], true);
331 
332     // Six evictions, twelve evicted (3 new)
333     assertEquals(6, cache.getEvictionCount());
334     assertEquals(12, cache.getEvictedCount());
335 
336     // Expect three remaining in-memory to be evicted
337     assertEquals(null, cache.getBlock(memoryBlocks[1].blockName, true));
338     assertEquals(null, cache.getBlock(memoryBlocks[2].blockName, true));
339     assertEquals(null, cache.getBlock(memoryBlocks[3].blockName, true));
340 
341 
342   }
343 
344   // test scan resistance
345   public void testScanResistance() throws Exception {
346 
347     long maxSize = 100000;
348     long blockSize = calculateBlockSize(maxSize, 10);
349 
350     LruBlockCache cache = new LruBlockCache(maxSize, blockSize, false,
351         (int)Math.ceil(1.2*maxSize/blockSize),
352         LruBlockCache.DEFAULT_LOAD_FACTOR,
353         LruBlockCache.DEFAULT_CONCURRENCY_LEVEL,
354         0.66f, // min
355         0.99f, // acceptable
356         0.33f, // single
357         0.33f, // multi
358         0.34f);// memory
359 
360     CachedItem [] singleBlocks = generateFixedBlocks(20, blockSize, "single");
361     CachedItem [] multiBlocks = generateFixedBlocks(5, blockSize, "multi");
362 
363     // Add 5 multi blocks
364     for (CachedItem block : multiBlocks) {
365       cache.cacheBlock(block.blockName, block);
366       cache.getBlock(block.blockName, true);
367     }
368 
369     // Add 5 single blocks
370     for(int i=0;i<5;i++) {
371       cache.cacheBlock(singleBlocks[i].blockName, singleBlocks[i]);
372     }
373 
374     // An eviction ran
375     assertEquals(1, cache.getEvictionCount());
376 
377     // To drop down to 2/3 capacity, we'll need to evict 4 blocks
378     assertEquals(4, cache.getEvictedCount());
379 
380     // Should have been taken off equally from single and multi
381     assertEquals(null, cache.getBlock(singleBlocks[0].blockName, true));
382     assertEquals(null, cache.getBlock(singleBlocks[1].blockName, true));
383     assertEquals(null, cache.getBlock(multiBlocks[0].blockName, true));
384     assertEquals(null, cache.getBlock(multiBlocks[1].blockName, true));
385 
386     // Let's keep "scanning" by adding single blocks.  From here on we only
387     // expect evictions from the single bucket.
388 
389     // Every time we reach 10 total blocks (every 4 inserts) we get 4 single
390     // blocks evicted.  Inserting 13 blocks should yield 3 more evictions and
391     // 12 more evicted.
392 
393     for(int i=5;i<18;i++) {
394       cache.cacheBlock(singleBlocks[i].blockName, singleBlocks[i]);
395     }
396 
397     // 4 total evictions, 16 total evicted
398     assertEquals(4, cache.getEvictionCount());
399     assertEquals(16, cache.getEvictedCount());
400 
401     // Should now have 7 total blocks
402     assertEquals(7, cache.size());
403 
404   }
405 
406   // test setMaxSize
407   public void testResizeBlockCache() throws Exception {
408 
409     long maxSize = 300000;
410     long blockSize = calculateBlockSize(maxSize, 31);
411 
412     LruBlockCache cache = new LruBlockCache(maxSize, blockSize, false,
413         (int)Math.ceil(1.2*maxSize/blockSize),
414         LruBlockCache.DEFAULT_LOAD_FACTOR,
415         LruBlockCache.DEFAULT_CONCURRENCY_LEVEL,
416         0.98f, // min
417         0.99f, // acceptable
418         0.33f, // single
419         0.33f, // multi
420         0.34f);// memory
421 
422     CachedItem [] singleBlocks = generateFixedBlocks(10, blockSize, "single");
423     CachedItem [] multiBlocks = generateFixedBlocks(10, blockSize, "multi");
424     CachedItem [] memoryBlocks = generateFixedBlocks(10, blockSize, "memory");
425 
426     // Add all blocks from all priorities
427     for(int i=0;i<10;i++) {
428 
429       // Just add single blocks
430       cache.cacheBlock(singleBlocks[i].blockName, singleBlocks[i]);
431 
432       // Add and get multi blocks
433       cache.cacheBlock(multiBlocks[i].blockName, multiBlocks[i]);
434       cache.getBlock(multiBlocks[i].blockName, true);
435 
436       // Add memory blocks as such
437       cache.cacheBlock(memoryBlocks[i].blockName, memoryBlocks[i], true);
438     }
439 
440     // Do not expect any evictions yet
441     assertEquals(0, cache.getEvictionCount());
442 
443     // Resize to half capacity plus an extra block (otherwise we evict an extra)
444     cache.setMaxSize((long)(maxSize * 0.5f));
445 
446     // Should have run a single eviction
447     assertEquals(1, cache.getEvictionCount());
448 
449     // And we expect 1/2 of the blocks to be evicted
450     assertEquals(15, cache.getEvictedCount());
451 
452     // And the oldest 5 blocks from each category should be gone
453     for(int i=0;i<5;i++) {
454       assertEquals(null, cache.getBlock(singleBlocks[i].blockName, true));
455       assertEquals(null, cache.getBlock(multiBlocks[i].blockName, true));
456       assertEquals(null, cache.getBlock(memoryBlocks[i].blockName, true));
457     }
458 
459     // And the newest 5 blocks should still be accessible
460     for(int i=5;i<10;i++) {
461       assertEquals(singleBlocks[i], cache.getBlock(singleBlocks[i].blockName, true));
462       assertEquals(multiBlocks[i], cache.getBlock(multiBlocks[i].blockName, true));
463       assertEquals(memoryBlocks[i], cache.getBlock(memoryBlocks[i].blockName, true));
464     }
465   }
466 
467   private CachedItem [] generateFixedBlocks(int numBlocks, int size, String pfx) {
468     CachedItem [] blocks = new CachedItem[numBlocks];
469     for(int i=0;i<numBlocks;i++) {
470       blocks[i] = new CachedItem(pfx + i, size);
471     }
472     return blocks;
473   }
474 
475   private CachedItem [] generateFixedBlocks(int numBlocks, long size, String pfx) {
476     return generateFixedBlocks(numBlocks, (int)size, pfx);
477   }
478 
479   private CachedItem [] generateRandomBlocks(int numBlocks, long maxSize) {
480     CachedItem [] blocks = new CachedItem[numBlocks];
481     Random r = new Random();
482     for(int i=0;i<numBlocks;i++) {
483       blocks[i] = new CachedItem("block" + i, r.nextInt((int)maxSize)+1);
484     }
485     return blocks;
486   }
487 
488   private long calculateBlockSize(long maxSize, int numBlocks) {
489     long roughBlockSize = maxSize / numBlocks;
490     int numEntries = (int)Math.ceil((1.2)*maxSize/roughBlockSize);
491     long totalOverhead = LruBlockCache.CACHE_FIXED_OVERHEAD +
492         ClassSize.CONCURRENT_HASHMAP +
493         (numEntries * ClassSize.CONCURRENT_HASHMAP_ENTRY) +
494         (LruBlockCache.DEFAULT_CONCURRENCY_LEVEL * ClassSize.CONCURRENT_HASHMAP_SEGMENT);
495     long negateBlockSize = (long)(totalOverhead/numEntries);
496     negateBlockSize += CachedBlock.PER_BLOCK_OVERHEAD;
497     return ClassSize.align((long)Math.floor((roughBlockSize - negateBlockSize)*0.99f));
498   }
499 
500   private long calculateBlockSizeDefault(long maxSize, int numBlocks) {
501     long roughBlockSize = maxSize / numBlocks;
502     int numEntries = (int)Math.ceil((1.2)*maxSize/roughBlockSize);
503     long totalOverhead = LruBlockCache.CACHE_FIXED_OVERHEAD +
504         ClassSize.CONCURRENT_HASHMAP +
505         (numEntries * ClassSize.CONCURRENT_HASHMAP_ENTRY) +
506         (LruBlockCache.DEFAULT_CONCURRENCY_LEVEL * ClassSize.CONCURRENT_HASHMAP_SEGMENT);
507     long negateBlockSize = totalOverhead / numEntries;
508     negateBlockSize += CachedBlock.PER_BLOCK_OVERHEAD;
509     return ClassSize.align((long)Math.floor((roughBlockSize - negateBlockSize)*
510         LruBlockCache.DEFAULT_ACCEPTABLE_FACTOR));
511   }
512 
513   private static class CachedItem implements Cacheable {
514     String blockName;
515     int size;
516 
517     CachedItem(String blockName, int size) {
518       this.blockName = blockName;
519       this.size = size;
520     }
521 
522     /** The size of this item reported to the block cache layer */
523     @Override
524     public long heapSize() {
525       return ClassSize.align(size);
526     }
527 
528     /** Size of the cache block holding this item. Used for verification. */
529     public long cacheBlockHeapSize() {
530       return CachedBlock.PER_BLOCK_OVERHEAD
531           + ClassSize.align(blockName.length())
532           + ClassSize.align(size);
533     }
534 
535     @Override
536     public int getSerializedLength() {
537       return 0;
538     }
539 
540     @Override
541     public CacheableDeserializer<Cacheable> getDeserializer() {
542       return null;
543     }
544 
545     @Override
546     public void serialize(ByteBuffer destination) {
547     }
548 
549   }
550 }