1   /*
2    * Copyright 2011 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  
21  package org.apache.hadoop.hbase.io.hfile;
22  
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.EnumMap;
27  import java.util.List;
28  import java.util.Random;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.hadoop.conf.Configuration;
33  import org.apache.hadoop.fs.FileSystem;
34  import org.apache.hadoop.fs.Path;
35  import org.apache.hadoop.hbase.HBaseTestingUtility;
36  import org.apache.hadoop.hbase.KeyValue;
37  import org.apache.hadoop.hbase.regionserver.StoreFile;
38  import org.apache.hadoop.hbase.util.BloomFilterFactory;
39  import org.junit.After;
40  import org.junit.Before;
41  import org.junit.Test;
42  import org.junit.runner.RunWith;
43  import org.junit.runners.Parameterized;
44  import org.junit.runners.Parameterized.Parameters;
45  import static org.junit.Assert.*;
46  
47  /**
48   * Tests {@link HFile} cache-on-write functionality for the following block
49   * types: data blocks, non-root index blocks, and Bloom filter blocks.
50   */
51  @RunWith(Parameterized.class)
52  public class TestCacheOnWrite {
53  
54    private static final Log LOG = LogFactory.getLog(TestCacheOnWrite.class);
55  
56    private static final HBaseTestingUtility TEST_UTIL =
57      new HBaseTestingUtility();
58    private Configuration conf;
59    private CacheConfig cacheConf;
60    private FileSystem fs;
61    private Random rand = new Random(12983177L);
62    private Path storeFilePath;
63    private Compression.Algorithm compress;
64    private CacheOnWriteType cowType;
65    private BlockCache blockCache;
66    private String testName;
67  
68    private static final int DATA_BLOCK_SIZE = 2048;
69    private static final int NUM_KV = 25000;
70    private static final int INDEX_BLOCK_SIZE = 512;
71    private static final int BLOOM_BLOCK_SIZE = 4096;
72  
73    /** The number of valid key types possible in a store file */
74    private static final int NUM_VALID_KEY_TYPES =
75        KeyValue.Type.values().length - 2;
76  
77    private static enum CacheOnWriteType {
78      DATA_BLOCKS(BlockType.DATA, CacheConfig.CACHE_BLOCKS_ON_WRITE_KEY),
79      BLOOM_BLOCKS(BlockType.BLOOM_CHUNK,
80          CacheConfig.CACHE_BLOOM_BLOCKS_ON_WRITE_KEY),
81      INDEX_BLOCKS(BlockType.LEAF_INDEX,
82          CacheConfig.CACHE_INDEX_BLOCKS_ON_WRITE_KEY);
83  
84      private final String confKey;
85      private final BlockType inlineBlockType;
86  
87      private CacheOnWriteType(BlockType inlineBlockType, String confKey) {
88        this.inlineBlockType = inlineBlockType;
89        this.confKey = confKey;
90      }
91  
92      public boolean shouldBeCached(BlockType blockType) {
93        return blockType == inlineBlockType
94            || blockType == BlockType.INTERMEDIATE_INDEX
95            && inlineBlockType == BlockType.LEAF_INDEX;
96      }
97  
98      public void modifyConf(Configuration conf) {
99        for (CacheOnWriteType cowType : CacheOnWriteType.values())
100         conf.setBoolean(cowType.confKey, cowType == this);
101     }
102 
103   }
104 
105   public TestCacheOnWrite(CacheOnWriteType cowType,
106       Compression.Algorithm compress) {
107     this.cowType = cowType;
108     this.compress = compress;
109     testName = "[cacheOnWrite=" + cowType + ", compress=" + compress + "]";
110     System.out.println(testName);
111   }
112 
113   @Parameters
114   public static Collection<Object[]> getParameters() {
115     List<Object[]> cowTypes = new ArrayList<Object[]>();
116     for (CacheOnWriteType cowType : CacheOnWriteType.values())
117       for (Compression.Algorithm compress :
118            HBaseTestingUtility.COMPRESSION_ALGORITHMS) {
119         cowTypes.add(new Object[] { cowType, compress });
120       }
121     return cowTypes;
122   }
123 
124   @Before
125   public void setUp() throws IOException {
126     conf = TEST_UTIL.getConfiguration();
127     conf.setInt(HFile.FORMAT_VERSION_KEY, HFile.MAX_FORMAT_VERSION);
128     conf.setInt(HFileBlockIndex.MAX_CHUNK_SIZE_KEY, INDEX_BLOCK_SIZE);
129     conf.setInt(BloomFilterFactory.IO_STOREFILE_BLOOM_BLOCK_SIZE,
130         BLOOM_BLOCK_SIZE);
131     conf.setBoolean(CacheConfig.CACHE_BLOCKS_ON_WRITE_KEY,
132         cowType.shouldBeCached(BlockType.DATA));
133     conf.setBoolean(CacheConfig.CACHE_INDEX_BLOCKS_ON_WRITE_KEY,
134         cowType.shouldBeCached(BlockType.LEAF_INDEX));
135     conf.setBoolean(CacheConfig.CACHE_BLOOM_BLOCKS_ON_WRITE_KEY,
136         cowType.shouldBeCached(BlockType.BLOOM_CHUNK));
137     cowType.modifyConf(conf);
138     fs = FileSystem.get(conf);
139     cacheConf = new CacheConfig(conf);
140     blockCache = cacheConf.getBlockCache();
141     System.out.println("setUp()");
142   }
143 
144   @After
145   public void tearDown() {
146     blockCache.evictBlocksByPrefix("");
147   }
148 
149   @Test
150   public void testCacheOnWrite() throws IOException {
151     writeStoreFile();
152     readStoreFile();
153   }
154 
155   private void readStoreFile() throws IOException {
156     HFileReaderV2 reader = (HFileReaderV2) HFile.createReader(fs,
157         storeFilePath, cacheConf);
158     LOG.info("HFile information: " + reader);
159     HFileScanner scanner = reader.getScanner(false, false);
160     assertTrue(testName, scanner.seekTo());
161 
162     long offset = 0;
163     HFileBlock prevBlock = null;
164     EnumMap<BlockType, Integer> blockCountByType =
165         new EnumMap<BlockType, Integer>(BlockType.class);
166 
167     while (offset < reader.getTrailer().getLoadOnOpenDataOffset()) {
168       long onDiskSize = -1;
169       if (prevBlock != null) {
170          onDiskSize = prevBlock.getNextBlockOnDiskSizeWithHeader();
171       }
172       // Flags: don't cache the block, use pread, this is not a compaction.
173       HFileBlock block = reader.readBlock(offset, onDiskSize, false, true,
174           false);
175       String blockCacheKey = HFile.getBlockCacheKey(reader.getName(), offset);
176       boolean isCached = blockCache.getBlock(blockCacheKey, true) != null;
177       boolean shouldBeCached = cowType.shouldBeCached(block.getBlockType());
178       assertEquals(testName + " " + block, shouldBeCached, isCached);
179       prevBlock = block;
180       offset += block.getOnDiskSizeWithHeader();
181       BlockType bt = block.getBlockType();
182       Integer count = blockCountByType.get(bt);
183       blockCountByType.put(bt, (count == null ? 0 : count) + 1);
184     }
185 
186     LOG.info("Block count by type: " + blockCountByType);
187     String countByType = blockCountByType.toString();
188     assertEquals(
189         "{DATA=1379, LEAF_INDEX=173, BLOOM_CHUNK=9, INTERMEDIATE_INDEX=24}",
190         countByType);
191 
192     reader.close();
193   }
194 
195   public static KeyValue.Type generateKeyType(Random rand) {
196     if (rand.nextBoolean()) {
197       // Let's make half of KVs puts.
198       return KeyValue.Type.Put;
199     } else {
200       KeyValue.Type keyType =
201           KeyValue.Type.values()[1 + rand.nextInt(NUM_VALID_KEY_TYPES)];
202       if (keyType == KeyValue.Type.Minimum || keyType == KeyValue.Type.Maximum)
203       {
204         throw new RuntimeException("Generated an invalid key type: " + keyType
205             + ". " + "Probably the layout of KeyValue.Type has changed.");
206       }
207       return keyType;
208     }
209   }
210 
211   public void writeStoreFile() throws IOException {
212     Path storeFileParentDir = new Path(TEST_UTIL.getDataTestDir(),
213         "test_cache_on_write");
214     StoreFile.Writer sfw = StoreFile.createWriter(fs, storeFileParentDir,
215         DATA_BLOCK_SIZE, compress, KeyValue.COMPARATOR, conf,
216         cacheConf, StoreFile.BloomType.ROWCOL, NUM_KV);
217 
218     final int rowLen = 32;
219     for (int i = 0; i < NUM_KV; ++i) {
220       byte[] k = TestHFileWriterV2.randomOrderedKey(rand, i);
221       byte[] v = TestHFileWriterV2.randomValue(rand);
222       int cfLen = rand.nextInt(k.length - rowLen + 1);
223       KeyValue kv = new KeyValue(
224           k, 0, rowLen,
225           k, rowLen, cfLen,
226           k, rowLen + cfLen, k.length - rowLen - cfLen,
227           rand.nextLong(),
228           generateKeyType(rand),
229           v, 0, v.length);
230       sfw.append(kv);
231     }
232 
233     sfw.close();
234     storeFilePath = sfw.getPath();
235   }
236 
237 }