View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with this
4    * work for additional information regarding copyright ownership. The ASF
5    * licenses this file to you under the Apache License, Version 2.0 (the
6    * "License"); you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14   * License for the specific language governing permissions and limitations
15   * under the License.
16   */
17  package org.apache.hadoop.hbase.io.encoding;
18  
19  import static org.junit.Assert.assertEquals;
20  import static org.junit.Assert.fail;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.DataInputStream;
25  import java.io.DataOutputStream;
26  import java.io.IOException;
27  import java.nio.ByteBuffer;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.List;
31  import java.util.Random;
32  
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.apache.hadoop.hbase.CategoryBasedTimeout;
36  import org.apache.hadoop.hbase.HBaseTestingUtility;
37  import org.apache.hadoop.hbase.HConstants;
38  import org.apache.hadoop.hbase.KeyValue;
39  import org.apache.hadoop.hbase.KeyValue.Type;
40  import org.apache.hadoop.hbase.KeyValueUtil;
41  import org.apache.hadoop.hbase.Tag;
42  import org.apache.hadoop.hbase.io.compress.Compression;
43  import org.apache.hadoop.hbase.io.hfile.HFileBlock.Writer.BufferGrabbingByteArrayOutputStream;
44  import org.apache.hadoop.hbase.io.hfile.HFileContext;
45  import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder;
46  import org.apache.hadoop.hbase.testclassification.LargeTests;
47  import org.apache.hadoop.hbase.util.Bytes;
48  import org.apache.hadoop.hbase.util.test.RedundantKVGenerator;
49  import org.junit.Rule;
50  import org.junit.Test;
51  import org.junit.experimental.categories.Category;
52  import org.junit.rules.TestRule;
53  import org.junit.runner.RunWith;
54  import org.junit.runners.Parameterized;
55  import org.junit.runners.Parameterized.Parameters;
56  
57  /**
58   * Test all of the data block encoding algorithms for correctness. Most of the
59   * class generate data which will test different branches in code.
60   */
61  @Category(LargeTests.class)
62  @RunWith(Parameterized.class)
63  public class TestDataBlockEncoders {
64  
65    private static final Log LOG = LogFactory.getLog(TestDataBlockEncoders.class);
66  
67    @Rule public final TestRule timeout = CategoryBasedTimeout.builder().
68        withTimeout(this.getClass()).withLookingForStuckThread(true).build();
69  
70    private static int NUMBER_OF_KV = 10000;
71    private static int NUM_RANDOM_SEEKS = 1000;
72  
73    private static int ENCODED_DATA_OFFSET = HConstants.HFILEBLOCK_HEADER_SIZE
74        + DataBlockEncoding.ID_SIZE;
75    static final byte[] HFILEBLOCK_DUMMY_HEADER = new byte[HConstants.HFILEBLOCK_HEADER_SIZE];
76  
77    private RedundantKVGenerator generator = new RedundantKVGenerator();
78    private Random randomizer = new Random(42l);
79  
80    private final boolean includesMemstoreTS;
81    private final boolean includesTags;
82  
83    @Parameters
84    public static Collection<Object[]> parameters() {
85      return HBaseTestingUtility.MEMSTORETS_TAGS_PARAMETRIZED;
86    }
87    public TestDataBlockEncoders(boolean includesMemstoreTS, boolean includesTag) {
88      this.includesMemstoreTS = includesMemstoreTS;
89      this.includesTags = includesTag;
90    }
91  
92    private HFileBlockEncodingContext getEncodingContext(Compression.Algorithm algo,
93        DataBlockEncoding encoding) {
94      DataBlockEncoder encoder = encoding.getEncoder();
95      HFileContext meta = new HFileContextBuilder()
96                          .withHBaseCheckSum(false)
97                          .withIncludesMvcc(includesMemstoreTS)
98                          .withIncludesTags(includesTags)
99                          .withCompression(algo).build();
100     if (encoder != null) {
101       return encoder.newDataBlockEncodingContext(encoding, HFILEBLOCK_DUMMY_HEADER, meta);
102     } else {
103       return new HFileBlockDefaultEncodingContext(encoding, HFILEBLOCK_DUMMY_HEADER, meta);
104     }
105   }
106 
107   /**
108    * Test data block encoding of empty KeyValue.
109    *
110    * @throws IOException
111    *           On test failure.
112    */
113   @Test
114   public void testEmptyKeyValues() throws IOException {
115     List<KeyValue> kvList = new ArrayList<KeyValue>();
116     byte[] row = new byte[0];
117     byte[] family = new byte[0];
118     byte[] qualifier = new byte[0];
119     byte[] value = new byte[0];
120     if (!includesTags) {
121       kvList.add(new KeyValue(row, family, qualifier, 0l, value));
122       kvList.add(new KeyValue(row, family, qualifier, 0l, value));
123     } else {
124       byte[] metaValue1 = Bytes.toBytes("metaValue1");
125       byte[] metaValue2 = Bytes.toBytes("metaValue2");
126       kvList.add(new KeyValue(row, family, qualifier, 0l, value, new Tag[] { new Tag((byte) 1,
127           metaValue1) }));
128       kvList.add(new KeyValue(row, family, qualifier, 0l, value, new Tag[] { new Tag((byte) 1,
129           metaValue2) }));
130     }
131     testEncodersOnDataset(kvList, includesMemstoreTS, includesTags);
132   }
133 
134   /**
135    * Test KeyValues with negative timestamp.
136    *
137    * @throws IOException
138    *           On test failure.
139    */
140   @Test
141   public void testNegativeTimestamps() throws IOException {
142     List<KeyValue> kvList = new ArrayList<KeyValue>();
143     byte[] row = new byte[0];
144     byte[] family = new byte[0];
145     byte[] qualifier = new byte[0];
146     byte[] value = new byte[0];
147     if (includesTags) {
148       byte[] metaValue1 = Bytes.toBytes("metaValue1");
149       byte[] metaValue2 = Bytes.toBytes("metaValue2");
150       kvList.add(new KeyValue(row, family, qualifier, 0l, value, new Tag[] { new Tag((byte) 1,
151           metaValue1) }));
152       kvList.add(new KeyValue(row, family, qualifier, 0l, value, new Tag[] { new Tag((byte) 1,
153           metaValue2) }));
154     } else {
155       kvList.add(new KeyValue(row, family, qualifier, -1l, Type.Put, value));
156       kvList.add(new KeyValue(row, family, qualifier, -2l, Type.Put, value));
157     }
158     testEncodersOnDataset(kvList, includesMemstoreTS, includesTags);
159   }
160 
161 
162   /**
163    * Test whether compression -> decompression gives the consistent results on
164    * pseudorandom sample.
165    * @throws IOException On test failure.
166    */
167   @Test
168   public void testExecutionOnSample() throws IOException {
169     List<KeyValue> kvList = generator.generateTestKeyValues(NUMBER_OF_KV, includesTags);
170     testEncodersOnDataset(kvList, includesMemstoreTS, includesTags);
171   }
172 
173   /**
174    * Test seeking while file is encoded.
175    */
176   @Test
177   public void testSeekingOnSample() throws IOException {
178     List<KeyValue> sampleKv = generator.generateTestKeyValues(NUMBER_OF_KV, includesTags);
179 
180     // create all seekers
181     List<DataBlockEncoder.EncodedSeeker> encodedSeekers =
182         new ArrayList<DataBlockEncoder.EncodedSeeker>();
183     for (DataBlockEncoding encoding : DataBlockEncoding.values()) {
184       LOG.info("Encoding: " + encoding);
185       // Off heap block data support not added for PREFIX_TREE DBE yet.
186       // TODO remove this once support is added. HBASE-12298
187       if (encoding == DataBlockEncoding.PREFIX_TREE) continue;
188       DataBlockEncoder encoder = encoding.getEncoder();
189       if (encoder == null) {
190         continue;
191       }
192       LOG.info("Encoder: " + encoder);
193       ByteBuffer encodedBuffer = encodeKeyValues(encoding, sampleKv,
194           getEncodingContext(Compression.Algorithm.NONE, encoding));
195       HFileContext meta = new HFileContextBuilder()
196                           .withHBaseCheckSum(false)
197                           .withIncludesMvcc(includesMemstoreTS)
198                           .withIncludesTags(includesTags)
199                           .withCompression(Compression.Algorithm.NONE)
200                           .build();
201       DataBlockEncoder.EncodedSeeker seeker = encoder.createSeeker(KeyValue.COMPARATOR,
202           encoder.newDataBlockDecodingContext(meta));
203       seeker.setCurrentBuffer(encodedBuffer);
204       encodedSeekers.add(seeker);
205     }
206     LOG.info("Testing it!");
207     // test it!
208     // try a few random seeks
209     for (boolean seekBefore : new boolean[] { false, true }) {
210       for (int i = 0; i < NUM_RANDOM_SEEKS; ++i) {
211         int keyValueId;
212         if (!seekBefore) {
213           keyValueId = randomizer.nextInt(sampleKv.size());
214         } else {
215           keyValueId = randomizer.nextInt(sampleKv.size() - 1) + 1;
216         }
217 
218         KeyValue keyValue = sampleKv.get(keyValueId);
219         checkSeekingConsistency(encodedSeekers, seekBefore, keyValue);
220       }
221     }
222 
223     // check edge cases
224     LOG.info("Checking edge cases");
225     checkSeekingConsistency(encodedSeekers, false, sampleKv.get(0));
226     for (boolean seekBefore : new boolean[] { false, true }) {
227       checkSeekingConsistency(encodedSeekers, seekBefore, sampleKv.get(sampleKv.size() - 1));
228       KeyValue midKv = sampleKv.get(sampleKv.size() / 2);
229       KeyValue lastMidKv =KeyValueUtil.createLastOnRowCol(midKv);
230       checkSeekingConsistency(encodedSeekers, seekBefore, lastMidKv);
231     }
232     LOG.info("Done");
233   }
234 
235   static ByteBuffer encodeKeyValues(DataBlockEncoding encoding, List<KeyValue> kvs,
236       HFileBlockEncodingContext encodingContext) throws IOException {
237     DataBlockEncoder encoder = encoding.getEncoder();
238     ByteArrayOutputStream baos = new ByteArrayOutputStream();
239     baos.write(HFILEBLOCK_DUMMY_HEADER);
240     DataOutputStream dos = new DataOutputStream(baos);
241     encoder.startBlockEncoding(encodingContext, dos);
242     for (KeyValue kv : kvs) {
243       encoder.encode(kv, encodingContext, dos);
244     }
245     BufferGrabbingByteArrayOutputStream stream = new BufferGrabbingByteArrayOutputStream();
246     baos.writeTo(stream);
247     encoder.endBlockEncoding(encodingContext, dos, stream.getBuffer());
248     byte[] encodedData = new byte[baos.size() - ENCODED_DATA_OFFSET];
249     System.arraycopy(baos.toByteArray(), ENCODED_DATA_OFFSET, encodedData, 0, encodedData.length);
250     return ByteBuffer.wrap(encodedData);
251   }
252 
253   @Test
254   public void testNextOnSample() throws IOException {
255     List<KeyValue> sampleKv = generator.generateTestKeyValues(NUMBER_OF_KV, includesTags);
256 
257     for (DataBlockEncoding encoding : DataBlockEncoding.values()) {
258       if (encoding.getEncoder() == null) {
259         continue;
260       }
261       DataBlockEncoder encoder = encoding.getEncoder();
262       ByteBuffer encodedBuffer = encodeKeyValues(encoding, sampleKv,
263           getEncodingContext(Compression.Algorithm.NONE, encoding));
264       HFileContext meta = new HFileContextBuilder()
265                           .withHBaseCheckSum(false)
266                           .withIncludesMvcc(includesMemstoreTS)
267                           .withIncludesTags(includesTags)
268                           .withCompression(Compression.Algorithm.NONE)
269                           .build();
270       DataBlockEncoder.EncodedSeeker seeker = encoder.createSeeker(KeyValue.COMPARATOR,
271           encoder.newDataBlockDecodingContext(meta));
272       seeker.setCurrentBuffer(encodedBuffer);
273       int i = 0;
274       do {
275         KeyValue expectedKeyValue = sampleKv.get(i);
276         KeyValue keyValue = KeyValueUtil.copyToNewKeyValue(seeker.getKeyValue());
277         if (0 != Bytes.compareTo(keyValue.getBuffer(), keyValue.getOffset(), keyValue.getLength(),
278             expectedKeyValue.getBuffer(), expectedKeyValue.getOffset(),
279             expectedKeyValue.getLength())) {
280 
281           int commonPrefix = 0;
282           byte[] left = keyValue.getBuffer();
283           byte[] right = expectedKeyValue.getBuffer();
284           int leftOff = keyValue.getOffset();
285           int rightOff = expectedKeyValue.getOffset();
286           int length = Math.min(keyValue.getLength(), expectedKeyValue.getLength());
287           while (commonPrefix < length
288               && left[commonPrefix + leftOff] == right[commonPrefix + rightOff]) {
289             commonPrefix++;
290           }
291 
292           fail(String.format("next() produces wrong results "
293               + "encoder: %s i: %d commonPrefix: %d" + "\n expected %s\n actual      %s", encoder
294               .toString(), i, commonPrefix, Bytes.toStringBinary(expectedKeyValue.getBuffer(),
295               expectedKeyValue.getOffset(), expectedKeyValue.getLength()), Bytes
296               .toStringBinary(keyValue.getBuffer())));
297         }
298         i++;
299       } while (seeker.next());
300     }
301   }
302 
303   /**
304    * Test whether the decompression of first key is implemented correctly.
305    * @throws IOException
306    */
307   @Test
308   public void testFirstKeyInBlockOnSample() throws IOException {
309     List<KeyValue> sampleKv = generator.generateTestKeyValues(NUMBER_OF_KV, includesTags);
310 
311     for (DataBlockEncoding encoding : DataBlockEncoding.values()) {
312       if (encoding.getEncoder() == null) {
313         continue;
314       }
315       DataBlockEncoder encoder = encoding.getEncoder();
316       ByteBuffer encodedBuffer = encodeKeyValues(encoding, sampleKv,
317           getEncodingContext(Compression.Algorithm.NONE, encoding));
318       ByteBuffer keyBuffer = encoder.getFirstKeyInBlock(encodedBuffer);
319       KeyValue firstKv = sampleKv.get(0);
320       if (0 != Bytes.compareTo(keyBuffer.array(), keyBuffer.arrayOffset(), keyBuffer.limit(),
321           firstKv.getBuffer(), firstKv.getKeyOffset(), firstKv.getKeyLength())) {
322 
323         int commonPrefix = 0;
324         int length = Math.min(keyBuffer.limit(), firstKv.getKeyLength());
325         while (commonPrefix < length
326             && keyBuffer.array()[keyBuffer.arrayOffset() + commonPrefix] == firstKv.getBuffer()[firstKv
327                 .getKeyOffset() + commonPrefix]) {
328           commonPrefix++;
329         }
330         fail(String.format("Bug in '%s' commonPrefix %d", encoder.toString(), commonPrefix));
331       }
332     }
333   }
334 
335   private void checkSeekingConsistency(List<DataBlockEncoder.EncodedSeeker> encodedSeekers,
336       boolean seekBefore, KeyValue keyValue) {
337     ByteBuffer expectedKeyValue = null;
338     ByteBuffer expectedKey = null;
339     ByteBuffer expectedValue = null;
340     for (DataBlockEncoder.EncodedSeeker seeker : encodedSeekers) {
341       seeker.seekToKeyInBlock(keyValue, seekBefore);
342       seeker.rewind();
343 
344       ByteBuffer actualKeyValue = KeyValueUtil.copyKeyToNewByteBuffer(seeker.getKeyValue());
345       ByteBuffer actualKey = seeker.getKeyDeepCopy();
346       ByteBuffer actualValue = seeker.getValueShallowCopy();
347 
348       if (expectedKeyValue != null) {
349         assertEquals(expectedKeyValue, actualKeyValue);
350       } else {
351         expectedKeyValue = actualKeyValue;
352       }
353 
354       if (expectedKey != null) {
355         assertEquals(expectedKey, actualKey);
356       } else {
357         expectedKey = actualKey;
358       }
359 
360       if (expectedValue != null) {
361         assertEquals(expectedValue, actualValue);
362       } else {
363         expectedValue = actualValue;
364       }
365     }
366   }
367 
368   private void testEncodersOnDataset(List<KeyValue> kvList, boolean includesMemstoreTS,
369       boolean includesTags) throws IOException {
370     ByteBuffer unencodedDataBuf = RedundantKVGenerator.convertKvToByteBuffer(kvList,
371         includesMemstoreTS);
372     HFileContext fileContext = new HFileContextBuilder().withIncludesMvcc(includesMemstoreTS)
373         .withIncludesTags(includesTags).build();
374     for (DataBlockEncoding encoding : DataBlockEncoding.values()) {
375       DataBlockEncoder encoder = encoding.getEncoder();
376       if (encoder == null) {
377         continue;
378       }
379       HFileBlockEncodingContext encodingContext = new HFileBlockDefaultEncodingContext(encoding,
380           HFILEBLOCK_DUMMY_HEADER, fileContext);
381 
382       ByteArrayOutputStream baos = new ByteArrayOutputStream();
383       baos.write(HFILEBLOCK_DUMMY_HEADER);
384       DataOutputStream dos = new DataOutputStream(baos);
385       encoder.startBlockEncoding(encodingContext, dos);
386       for (KeyValue kv : kvList) {
387         encoder.encode(kv, encodingContext, dos);
388       }
389       BufferGrabbingByteArrayOutputStream stream = new BufferGrabbingByteArrayOutputStream();
390       baos.writeTo(stream);
391       encoder.endBlockEncoding(encodingContext, dos, stream.getBuffer());
392       byte[] encodedData = baos.toByteArray();
393 
394       testAlgorithm(encodedData, unencodedDataBuf, encoder);
395     }
396   }
397 
398   @Test
399   public void testZeroByte() throws IOException {
400     List<KeyValue> kvList = new ArrayList<KeyValue>();
401     byte[] row = Bytes.toBytes("abcd");
402     byte[] family = new byte[] { 'f' };
403     byte[] qualifier0 = new byte[] { 'b' };
404     byte[] qualifier1 = new byte[] { 'c' };
405     byte[] value0 = new byte[] { 'd' };
406     byte[] value1 = new byte[] { 0x00 };
407     if (includesTags) {
408       kvList.add(new KeyValue(row, family, qualifier0, 0, value0, new Tag[] { new Tag((byte) 1,
409           "value1") }));
410       kvList.add(new KeyValue(row, family, qualifier1, 0, value1, new Tag[] { new Tag((byte) 1,
411           "value1") }));
412     } else {
413       kvList.add(new KeyValue(row, family, qualifier0, 0, Type.Put, value0));
414       kvList.add(new KeyValue(row, family, qualifier1, 0, Type.Put, value1));
415     }
416     testEncodersOnDataset(kvList, includesMemstoreTS, includesTags);
417   }
418 
419   private void testAlgorithm(byte[] encodedData, ByteBuffer unencodedDataBuf,
420       DataBlockEncoder encoder) throws IOException {
421     // decode
422     ByteArrayInputStream bais = new ByteArrayInputStream(encodedData, ENCODED_DATA_OFFSET,
423         encodedData.length - ENCODED_DATA_OFFSET);
424     DataInputStream dis = new DataInputStream(bais);
425     ByteBuffer actualDataset;
426     HFileContext meta = new HFileContextBuilder().withHBaseCheckSum(false)
427         .withIncludesMvcc(includesMemstoreTS).withIncludesTags(includesTags)
428         .withCompression(Compression.Algorithm.NONE).build();
429     actualDataset = encoder.decodeKeyValues(dis, encoder.newDataBlockDecodingContext(meta));
430     actualDataset.rewind();
431 
432     // this is because in case of prefix tree the decoded stream will not have
433     // the
434     // mvcc in it.
435     assertEquals("Encoding -> decoding gives different results for " + encoder,
436         Bytes.toStringBinary(unencodedDataBuf), Bytes.toStringBinary(actualDataset));
437   }
438 }