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}