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.assertTrue;
022import static org.junit.jupiter.api.Assertions.fail;
023
024import java.io.DataOutputStream;
025import java.io.IOException;
026import java.nio.ByteBuffer;
027import java.util.ArrayList;
028import java.util.List;
029import java.util.stream.Stream;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.hbase.ExtendedCell;
032import org.apache.hadoop.hbase.HBaseConfiguration;
033import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate;
034import org.apache.hadoop.hbase.HConstants;
035import org.apache.hadoop.hbase.KeyValue;
036import org.apache.hadoop.hbase.io.ByteArrayOutputStream;
037import org.apache.hadoop.hbase.io.ByteBuffAllocator;
038import org.apache.hadoop.hbase.io.HeapSize;
039import org.apache.hadoop.hbase.io.compress.Compression.Algorithm;
040import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
041import org.apache.hadoop.hbase.io.encoding.HFileBlockDefaultEncodingContext;
042import org.apache.hadoop.hbase.io.encoding.HFileBlockEncodingContext;
043import org.apache.hadoop.hbase.nio.ByteBuff;
044import org.apache.hadoop.hbase.testclassification.IOTests;
045import org.apache.hadoop.hbase.testclassification.MediumTests;
046import org.apache.hadoop.hbase.util.ChecksumType;
047import org.apache.hadoop.hbase.util.RedundantKVGenerator;
048import org.junit.jupiter.api.Tag;
049import org.junit.jupiter.api.TestTemplate;
050import org.junit.jupiter.params.provider.Arguments;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054@Tag(IOTests.TAG)
055@Tag(MediumTests.TAG)
056@HBaseParameterizedTestTemplate(name = "{index}: dataBlockEncoding={0}, includesMemstoreTS={1}")
057public class TestHFileDataBlockEncoder {
058
059  private static final Logger LOG = LoggerFactory.getLogger(TestHFileDataBlockEncoder.class);
060
061  private final Configuration conf = HBaseConfiguration.create();
062  private final RedundantKVGenerator generator = new RedundantKVGenerator();
063  private HFileDataBlockEncoder blockEncoder;
064  private boolean includesMemstoreTS;
065
066  /**
067   * Create test for given data block encoding configuration.
068   * @param blockEncoder What kind of encoding policy will be used.
069   */
070  public TestHFileDataBlockEncoder(HFileDataBlockEncoder blockEncoder, boolean includesMemstoreTS) {
071    this.blockEncoder = blockEncoder;
072    this.includesMemstoreTS = includesMemstoreTS;
073    LOG.info("Encoding: " + blockEncoder.getDataBlockEncoding() + ", includesMemstoreTS: "
074      + includesMemstoreTS);
075  }
076
077  /**
078   * Test putting and taking out blocks into cache with different encoding options.
079   */
080  @TestTemplate
081  public void testEncodingWithCache() throws IOException {
082    testEncodingWithCacheInternals(false);
083    testEncodingWithCacheInternals(true);
084  }
085
086  private void testEncodingWithCacheInternals(boolean useTag) throws IOException {
087    List<KeyValue> kvs = generator.generateTestKeyValues(60, useTag);
088    HFileBlock block = getSampleHFileBlock(kvs, useTag);
089    HFileBlock cacheBlock = createBlockOnDisk(conf, kvs, block, useTag);
090
091    LruBlockCache blockCache = new LruBlockCache(8 * 1024 * 1024, 32 * 1024);
092    BlockCacheKey cacheKey = new BlockCacheKey("test", 0);
093    blockCache.cacheBlock(cacheKey, cacheBlock);
094
095    HeapSize heapSize = blockCache.getBlock(cacheKey, false, false, true);
096    assertTrue(heapSize instanceof HFileBlock);
097
098    HFileBlock returnedBlock = (HFileBlock) heapSize;
099
100    if (blockEncoder.getDataBlockEncoding() == DataBlockEncoding.NONE) {
101      assertEquals(block.getBufferReadOnly(), returnedBlock.getBufferReadOnly());
102    } else {
103      if (BlockType.ENCODED_DATA != returnedBlock.getBlockType()) {
104        LOG.info("" + blockEncoder);
105      }
106      assertEquals(BlockType.ENCODED_DATA, returnedBlock.getBlockType());
107    }
108  }
109
110  /** Test for HBASE-5746. */
111  @TestTemplate
112  public void testHeaderSizeInCacheWithoutChecksum() throws Exception {
113    testHeaderSizeInCacheWithoutChecksumInternals(false);
114    testHeaderSizeInCacheWithoutChecksumInternals(true);
115  }
116
117  private void testHeaderSizeInCacheWithoutChecksumInternals(boolean useTags) throws IOException {
118    int headerSize = HConstants.HFILEBLOCK_HEADER_SIZE_NO_CHECKSUM;
119    // Create some KVs and create the block with old-style header.
120    List<KeyValue> kvs = generator.generateTestKeyValues(60, useTags);
121    ByteBuffer keyValues = RedundantKVGenerator.convertKvToByteBuffer(kvs, includesMemstoreTS);
122    int size = keyValues.limit();
123    ByteBuffer buf = ByteBuffer.allocate(size + headerSize);
124    buf.position(headerSize);
125    keyValues.rewind();
126    buf.put(keyValues);
127    HFileContext hfileContext =
128      new HFileContextBuilder().withHBaseCheckSum(false).withIncludesMvcc(includesMemstoreTS)
129        .withIncludesTags(useTags).withBlockSize(0).withChecksumType(ChecksumType.NULL).build();
130    HFileBlock block = new HFileBlock(BlockType.DATA, size, size, -1, ByteBuff.wrap(buf),
131      HFileBlock.FILL_HEADER, 0, 0, -1, hfileContext, ByteBuffAllocator.HEAP);
132    HFileBlock cacheBlock = createBlockOnDisk(conf, kvs, block, useTags);
133    assertEquals(headerSize, cacheBlock.getDummyHeaderForVersion().length);
134  }
135
136  /**
137   * Test encoding.
138   */
139  @TestTemplate
140  public void testEncoding() throws IOException {
141    testEncodingInternals(false);
142    testEncodingInternals(true);
143  }
144
145  /**
146   * Test encoding with offheap keyvalue. This test just verifies if the encoders work with DBB and
147   * does not use the getXXXArray() API
148   */
149  @TestTemplate
150  public void testEncodingWithOffheapKeyValue() throws IOException {
151    // usually we have just block without headers, but don't complicate that
152    try {
153      List<ExtendedCell> kvs = generator.generateTestExtendedOffheapKeyValues(60, true);
154      HFileContext meta = new HFileContextBuilder().withIncludesMvcc(includesMemstoreTS)
155        .withIncludesTags(true).withHBaseCheckSum(true).withCompression(Algorithm.NONE)
156        .withBlockSize(0).withChecksumType(ChecksumType.NULL).build();
157      writeBlock(conf, kvs, meta, true);
158    } catch (IllegalArgumentException e) {
159      fail("No exception should have been thrown");
160    }
161  }
162
163  private void testEncodingInternals(boolean useTag) throws IOException {
164    // usually we have just block without headers, but don't complicate that
165    List<KeyValue> kvs = generator.generateTestKeyValues(60, useTag);
166    HFileBlock block = getSampleHFileBlock(kvs, useTag);
167    HFileBlock blockOnDisk = createBlockOnDisk(conf, kvs, block, useTag);
168
169    if (blockEncoder.getDataBlockEncoding() != DataBlockEncoding.NONE) {
170      assertEquals(BlockType.ENCODED_DATA, blockOnDisk.getBlockType());
171      assertEquals(blockEncoder.getDataBlockEncoding().getId(),
172        blockOnDisk.getDataBlockEncodingId());
173    } else {
174      assertEquals(BlockType.DATA, blockOnDisk.getBlockType());
175    }
176  }
177
178  private HFileBlock getSampleHFileBlock(List<KeyValue> kvs, boolean useTag) {
179    ByteBuffer keyValues = RedundantKVGenerator.convertKvToByteBuffer(kvs, includesMemstoreTS);
180    int size = keyValues.limit();
181    ByteBuffer buf = ByteBuffer.allocate(size + HConstants.HFILEBLOCK_HEADER_SIZE);
182    buf.position(HConstants.HFILEBLOCK_HEADER_SIZE);
183    keyValues.rewind();
184    buf.put(keyValues);
185    HFileContext meta = new HFileContextBuilder().withIncludesMvcc(includesMemstoreTS)
186      .withIncludesTags(useTag).withHBaseCheckSum(true).withCompression(Algorithm.NONE)
187      .withBlockSize(0).withChecksumType(ChecksumType.NULL).build();
188    HFileBlock b = new HFileBlock(BlockType.DATA, size, size, -1, ByteBuff.wrap(buf),
189      HFileBlock.FILL_HEADER, 0, 0, -1, meta, ByteBuffAllocator.HEAP);
190    return b;
191  }
192
193  private HFileBlock createBlockOnDisk(Configuration conf, List<KeyValue> kvs, HFileBlock block,
194    boolean useTags) throws IOException {
195    int size;
196    HFileBlockEncodingContext context =
197      new HFileBlockDefaultEncodingContext(conf, blockEncoder.getDataBlockEncoding(),
198        HConstants.HFILEBLOCK_DUMMY_HEADER, block.getHFileContext());
199
200    ByteArrayOutputStream baos = new ByteArrayOutputStream();
201    baos.write(block.getDummyHeaderForVersion());
202    DataOutputStream dos = new DataOutputStream(baos);
203    blockEncoder.startBlockEncoding(context, dos);
204    for (KeyValue kv : kvs) {
205      blockEncoder.encode(kv, context, dos);
206    }
207    blockEncoder.endBlockEncoding(context, dos, baos.getBuffer(), BlockType.DATA);
208    byte[] encodedBytes = baos.toByteArray();
209    size = encodedBytes.length - block.getDummyHeaderForVersion().length;
210    return new HFileBlock(context.getBlockType(), size, size, -1,
211      ByteBuff.wrap(ByteBuffer.wrap(encodedBytes)), HFileBlock.FILL_HEADER, 0,
212      block.getOnDiskDataSizeWithHeader(), -1, block.getHFileContext(), ByteBuffAllocator.HEAP);
213  }
214
215  private void writeBlock(Configuration conf, List<ExtendedCell> kvs, HFileContext fileContext,
216    boolean useTags) throws IOException {
217    HFileBlockEncodingContext context = new HFileBlockDefaultEncodingContext(conf,
218      blockEncoder.getDataBlockEncoding(), HConstants.HFILEBLOCK_DUMMY_HEADER, fileContext);
219
220    ByteArrayOutputStream baos = new ByteArrayOutputStream();
221    baos.write(HConstants.HFILEBLOCK_DUMMY_HEADER);
222    DataOutputStream dos = new DataOutputStream(baos);
223    blockEncoder.startBlockEncoding(context, dos);
224    for (ExtendedCell kv : kvs) {
225      blockEncoder.encode(kv, context, dos);
226    }
227  }
228
229  public static Stream<Arguments> parameters() {
230    List<Arguments> configurations = new ArrayList<>();
231
232    for (DataBlockEncoding diskAlgo : DataBlockEncoding.values()) {
233      for (boolean includesMemstoreTS : new boolean[] { false, true }) {
234        HFileDataBlockEncoder dbe = (diskAlgo == DataBlockEncoding.NONE)
235          ? NoOpDataBlockEncoder.INSTANCE
236          : new HFileDataBlockEncoderImpl(diskAlgo);
237        configurations.add(Arguments.of(dbe, includesMemstoreTS));
238      }
239    }
240
241    return configurations.stream();
242  }
243}