/** * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hbase.io.hfile; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.MemoryUsage; import java.nio.ByteBuffer; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.testclassification.LargeTests; import org.apache.hadoop.hbase.io.hfile.bucket.BucketCache; import org.apache.hadoop.hbase.util.Threads; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; /** * Tests that {@link CacheConfig} does as expected. */ // This test is marked as a large test though it runs in a short amount of time // (seconds). It is large because it depends on being able to reset the global // blockcache instance which is in a global variable. Experience has it that // tests clash on the global variable if this test is run as small sized test. @Category(LargeTests.class) public class TestCacheConfig { private static final Log LOG = LogFactory.getLog(TestCacheConfig.class); private Configuration conf; static class Deserializer implements CacheableDeserializer<Cacheable> { private final Cacheable cacheable; private int deserializedIdentifier = 0; Deserializer(final Cacheable c) { deserializedIdentifier = CacheableDeserializerIdManager.registerDeserializer(this); this.cacheable = c; } @Override public int getDeserialiserIdentifier() { return deserializedIdentifier; } @Override public Cacheable deserialize(ByteBuffer b, boolean reuse) throws IOException { LOG.info("Deserialized " + b + ", reuse=" + reuse); return cacheable; } @Override public Cacheable deserialize(ByteBuffer b) throws IOException { LOG.info("Deserialized " + b); return cacheable; } }; static class IndexCacheEntry extends DataCacheEntry { private static IndexCacheEntry SINGLETON = new IndexCacheEntry(); public IndexCacheEntry() { super(SINGLETON); } @Override public BlockType getBlockType() { return BlockType.ROOT_INDEX; } } static class DataCacheEntry implements Cacheable { private static final int SIZE = 1; private static DataCacheEntry SINGLETON = new DataCacheEntry(); final CacheableDeserializer<Cacheable> deserializer; DataCacheEntry() { this(SINGLETON); } DataCacheEntry(final Cacheable c) { this.deserializer = new Deserializer(c); } @Override public String toString() { return "size=" + SIZE + ", type=" + getBlockType(); }; @Override public long heapSize() { return SIZE; } @Override public int getSerializedLength() { return SIZE; } @Override public void serialize(ByteBuffer destination) { LOG.info("Serialized " + this + " to " + destination); } @Override public CacheableDeserializer<Cacheable> getDeserializer() { return this.deserializer; } @Override public BlockType getBlockType() { return BlockType.DATA; } }; static class MetaCacheEntry extends DataCacheEntry { @Override public BlockType getBlockType() { return BlockType.INTERMEDIATE_INDEX; } } @Before public void setUp() throws Exception { CacheConfig.GLOBAL_BLOCK_CACHE_INSTANCE = null; this.conf = HBaseConfiguration.create(); } @After public void tearDown() throws Exception { // Let go of current block cache. CacheConfig.GLOBAL_BLOCK_CACHE_INSTANCE = null; } /** * @param cc * @param doubling If true, addition of element ups counter by 2, not 1, because element added * to onheap and offheap caches. * @param sizing True if we should run sizing test (doesn't always apply). */ void basicBlockCacheOps(final CacheConfig cc, final boolean doubling, final boolean sizing) { assertTrue(cc.isBlockCacheEnabled()); assertTrue(CacheConfig.DEFAULT_IN_MEMORY == cc.isInMemory()); BlockCache bc = cc.getBlockCache(); BlockCacheKey bck = new BlockCacheKey("f", 0); Cacheable c = new DataCacheEntry(); // Do asserts on block counting. long initialBlockCount = bc.getBlockCount(); bc.cacheBlock(bck, c, cc.isInMemory(), cc.isCacheDataInL1()); assertEquals(doubling? 2: 1, bc.getBlockCount() - initialBlockCount); bc.evictBlock(bck); assertEquals(initialBlockCount, bc.getBlockCount()); // Do size accounting. Do it after the above 'warm-up' because it looks like some // buffers do lazy allocation so sizes are off on first go around. if (sizing) { long originalSize = bc.getCurrentSize(); bc.cacheBlock(bck, c, cc.isInMemory(), cc.isCacheDataInL1()); assertTrue(bc.getCurrentSize() > originalSize); bc.evictBlock(bck); long size = bc.getCurrentSize(); assertEquals(originalSize, size); } } /** * @param cc * @param filename * @return */ private long cacheDataBlock(final CacheConfig cc, final String filename) { BlockCacheKey bck = new BlockCacheKey(filename, 0); Cacheable c = new DataCacheEntry(); // Do asserts on block counting. cc.getBlockCache().cacheBlock(bck, c, cc.isInMemory(), cc.isCacheDataInL1()); return cc.getBlockCache().getBlockCount(); } @Test public void testCacheConfigDefaultLRUBlockCache() { CacheConfig cc = new CacheConfig(this.conf); assertTrue(cc.isBlockCacheEnabled()); assertTrue(CacheConfig.DEFAULT_IN_MEMORY == cc.isInMemory()); basicBlockCacheOps(cc, false, true); assertTrue(cc.getBlockCache() instanceof LruBlockCache); } /** * Assert that the caches are deployed with CombinedBlockCache and of the appropriate sizes. */ @Test public void testOffHeapBucketCacheConfig() { this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap"); doBucketCacheConfigTest(); } @Test public void testOnHeapBucketCacheConfig() { this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "heap"); doBucketCacheConfigTest(); } @Test public void testFileBucketCacheConfig() throws IOException { HBaseTestingUtility htu = new HBaseTestingUtility(this.conf); try { Path p = new Path(htu.getDataTestDir(), "bc.txt"); FileSystem fs = FileSystem.get(this.conf); fs.create(p).close(); this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "file:" + p); doBucketCacheConfigTest(); } finally { htu.cleanupTestDir(); } } private void doBucketCacheConfigTest() { final int bcSize = 100; this.conf.setInt(HConstants.BUCKET_CACHE_SIZE_KEY, bcSize); CacheConfig cc = new CacheConfig(this.conf); basicBlockCacheOps(cc, false, false); assertTrue(cc.getBlockCache() instanceof CombinedBlockCache); // TODO: Assert sizes allocated are right and proportions. CombinedBlockCache cbc = (CombinedBlockCache)cc.getBlockCache(); BlockCache [] bcs = cbc.getBlockCaches(); assertTrue(bcs[0] instanceof LruBlockCache); LruBlockCache lbc = (LruBlockCache)bcs[0]; assertEquals(CacheConfig.getLruCacheSize(this.conf, ManagementFactory.getMemoryMXBean().getHeapMemoryUsage()), lbc.getMaxSize()); assertTrue(bcs[1] instanceof BucketCache); BucketCache bc = (BucketCache)bcs[1]; // getMaxSize comes back in bytes but we specified size in MB assertEquals(bcSize, bc.getMaxSize() / (1024 * 1024)); } /** * Assert that when BUCKET_CACHE_COMBINED_KEY is false, the non-default, that we deploy * LruBlockCache as L1 with a BucketCache for L2. */ @Test (timeout=10000) public void testBucketCacheConfigL1L2Setup() { this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap"); // Make lru size is smaller than bcSize for sure. Need this to be true so when eviction // from L1 happens, it does not fail because L2 can't take the eviction because block too big. this.conf.setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0.001f); MemoryUsage mu = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); long lruExpectedSize = CacheConfig.getLruCacheSize(this.conf, mu); final int bcSize = 100; long bcExpectedSize = 100 * 1024 * 1024; // MB. assertTrue(lruExpectedSize < bcExpectedSize); this.conf.setInt(HConstants.BUCKET_CACHE_SIZE_KEY, bcSize); this.conf.setBoolean(CacheConfig.BUCKET_CACHE_COMBINED_KEY, false); CacheConfig cc = new CacheConfig(this.conf); basicBlockCacheOps(cc, false, false); assertTrue(cc.getBlockCache() instanceof LruBlockCache); // TODO: Assert sizes allocated are right and proportions. LruBlockCache lbc = (LruBlockCache)cc.getBlockCache(); assertEquals(lruExpectedSize, lbc.getMaxSize()); BucketCache bc = lbc.getVictimHandler(); // getMaxSize comes back in bytes but we specified size in MB assertEquals(bcExpectedSize, bc.getMaxSize()); // Test the L1+L2 deploy works as we'd expect with blocks evicted from L1 going to L2. long initialL1BlockCount = lbc.getBlockCount(); long initialL2BlockCount = bc.getBlockCount(); Cacheable c = new DataCacheEntry(); BlockCacheKey bck = new BlockCacheKey("bck", 0); lbc.cacheBlock(bck, c, false, false); assertEquals(initialL1BlockCount + 1, lbc.getBlockCount()); assertEquals(initialL2BlockCount, bc.getBlockCount()); // Force evictions by putting in a block too big. final long justTooBigSize = lbc.acceptableSize() + 1; lbc.cacheBlock(new BlockCacheKey("bck2", 0), new DataCacheEntry() { @Override public long heapSize() { return justTooBigSize; } @Override public int getSerializedLength() { return (int)heapSize(); } }); // The eviction thread in lrublockcache needs to run. while (initialL1BlockCount != lbc.getBlockCount()) Threads.sleep(10); assertEquals(initialL1BlockCount, lbc.getBlockCount()); long count = bc.getBlockCount(); assertTrue(initialL2BlockCount + 1 <= count); } /** * Test the cacheDataInL1 flag. When set, data blocks should be cached in the l1 tier, up in * LruBlockCache when using CombinedBlockCcahe. */ @Test public void testCacheDataInL1() { this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap"); this.conf.setInt(HConstants.BUCKET_CACHE_SIZE_KEY, 100); CacheConfig cc = new CacheConfig(this.conf); assertTrue(cc.getBlockCache() instanceof CombinedBlockCache); CombinedBlockCache cbc = (CombinedBlockCache)cc.getBlockCache(); // Add a data block. Should go into L2, into the Bucket Cache, not the LruBlockCache. cacheDataBlock(cc, "1"); LruBlockCache lrubc = (LruBlockCache)cbc.getBlockCaches()[0]; assertDataBlockCount(lrubc, 0); // Enable our test flag. cc.setCacheDataInL1(true); cacheDataBlock(cc, "2"); assertDataBlockCount(lrubc, 1); cc.setCacheDataInL1(false); cacheDataBlock(cc, "3"); assertDataBlockCount(lrubc, 1); } private void assertDataBlockCount(final LruBlockCache bc, final int expected) { Map<BlockType, Integer> blocks = bc.getBlockTypeCountsForTest(); assertEquals(expected, blocks == null? 0: blocks.get(BlockType.DATA) == null? 0: blocks.get(BlockType.DATA).intValue()); } }