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