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}