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 java.io.ByteArrayInputStream;
021import java.io.DataInputStream;
022import java.io.DataOutputStream;
023import java.io.IOException;
024import java.io.SequenceInputStream;
025import java.security.Key;
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Comparator;
029import java.util.List;
030import java.util.Map;
031import java.util.Objects;
032import java.util.Set;
033import java.util.SortedMap;
034import java.util.TreeMap;
035import org.apache.commons.io.IOUtils;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.fs.Path;
038import org.apache.hadoop.hbase.Cell;
039import org.apache.hadoop.hbase.CellUtil;
040import org.apache.hadoop.hbase.ExtendedCell;
041import org.apache.hadoop.hbase.KeyValue;
042import org.apache.hadoop.hbase.io.crypto.Cipher;
043import org.apache.hadoop.hbase.io.crypto.Encryption;
044import org.apache.hadoop.hbase.protobuf.ProtobufMagic;
045import org.apache.hadoop.hbase.security.EncryptionUtil;
046import org.apache.hadoop.hbase.util.Bytes;
047import org.apache.yetus.audience.InterfaceAudience;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
052
053import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
054import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
055import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BytesBytesPair;
056import org.apache.hadoop.hbase.shaded.protobuf.generated.HFileProtos;
057
058/**
059 * Metadata Map of attributes for HFile written out as HFile Trailer. Created by the Writer and
060 * added to the tail of the file just before close. Metadata includes core attributes such as last
061 * key seen, comparator used writing the file, etc. Clients can add their own attributes via
062 * {@link #append(byte[], byte[], boolean)} and they'll be persisted and available at read time.
063 * Reader creates the HFileInfo on open by reading the tail of the HFile. The parse of the HFile
064 * trailer also creates a {@link HFileContext}, a read-only data structure that includes bulk of the
065 * HFileInfo and extras that is safe to pass around when working on HFiles.
066 * @see HFileContext
067 */
068@InterfaceAudience.Private
069public class HFileInfo implements SortedMap<byte[], byte[]> {
070
071  private static final Logger LOG = LoggerFactory.getLogger(HFileInfo.class);
072
073  static final String RESERVED_PREFIX = "hfile.";
074  static final byte[] RESERVED_PREFIX_BYTES = Bytes.toBytes(RESERVED_PREFIX);
075  static final byte[] LASTKEY = Bytes.toBytes(RESERVED_PREFIX + "LASTKEY");
076  static final byte[] AVG_KEY_LEN = Bytes.toBytes(RESERVED_PREFIX + "AVG_KEY_LEN");
077  static final byte[] AVG_VALUE_LEN = Bytes.toBytes(RESERVED_PREFIX + "AVG_VALUE_LEN");
078  static final byte[] CREATE_TIME_TS = Bytes.toBytes(RESERVED_PREFIX + "CREATE_TIME_TS");
079  static final byte[] TAGS_COMPRESSED = Bytes.toBytes(RESERVED_PREFIX + "TAGS_COMPRESSED");
080  static final byte[] KEY_OF_BIGGEST_CELL = Bytes.toBytes(RESERVED_PREFIX + "KEY_OF_BIGGEST_CELL");
081  static final byte[] LEN_OF_BIGGEST_CELL = Bytes.toBytes(RESERVED_PREFIX + "LEN_OF_BIGGEST_CELL");
082  public static final byte[] MAX_TAGS_LEN = Bytes.toBytes(RESERVED_PREFIX + "MAX_TAGS_LEN");
083  public static final byte[] FILE_SIZE = Bytes.toBytes(RESERVED_PREFIX + "FILE_SIZE");
084  public static final byte[] FILE_PATH = Bytes.toBytes(RESERVED_PREFIX + "FILE_PATH");
085  private final SortedMap<byte[], byte[]> map = new TreeMap<>(Bytes.BYTES_COMPARATOR);
086
087  /**
088   * We can read files whose major version is v2 IFF their minor version is at least 3.
089   */
090  private static final int MIN_V2_MINOR_VERSION_WITH_PB = 3;
091
092  /** Maximum minor version supported by this HFile format */
093  // We went to version 2 when we moved to pb'ing fileinfo and the trailer on
094  // the file. This version can read Writables version 1.
095  static final int MAX_MINOR_VERSION = 3;
096
097  /** Last key in the file. Filled in when we read in the file info */
098  private ExtendedCell lastKeyCell = null;
099  /** Average key length read from file info */
100  private int avgKeyLen = -1;
101  /** Average value length read from file info */
102  private int avgValueLen = -1;
103  /** Biggest Cell in the file, key only. Filled in when we read in the file info */
104  private Cell biggestCell = null;
105  /** Length of the biggest Cell */
106  private long lenOfBiggestCell = -1;
107  private boolean includesMemstoreTS = false;
108  private boolean decodeMemstoreTS = false;
109
110  /**
111   * Blocks read from the load-on-open section, excluding data root index, meta index, and file
112   * info.
113   */
114  private List<HFileBlock> loadOnOpenBlocks = new ArrayList<>();
115
116  /**
117   * The iterator will track all blocks in load-on-open section, since we use the
118   * {@link org.apache.hadoop.hbase.io.ByteBuffAllocator} to manage the ByteBuffers in block now, so
119   * we must ensure that deallocate all ByteBuffers in the end.
120   */
121  private HFileBlock.BlockIterator blockIter;
122
123  private HFileBlockIndex.CellBasedKeyBlockIndexReader dataIndexReader;
124  private HFileBlockIndex.ByteArrayKeyBlockIndexReader metaIndexReader;
125
126  private FixedFileTrailer trailer;
127  private HFileContext hfileContext;
128  private boolean initialized = false;
129
130  public HFileInfo() {
131    super();
132  }
133
134  public HFileInfo(ReaderContext context, Configuration conf) throws IOException {
135    this.initTrailerAndContext(context, conf);
136  }
137
138  /**
139   * Append the given key/value pair to the file info, optionally checking the key prefix.
140   * @param k           key to add
141   * @param v           value to add
142   * @param checkPrefix whether to check that the provided key does not start with the reserved
143   *                    prefix
144   * @return this file info object
145   * @throws IOException          if the key or value is invalid
146   * @throws NullPointerException if {@code key} or {@code value} is {@code null}
147   */
148  public HFileInfo append(final byte[] k, final byte[] v, final boolean checkPrefix)
149    throws IOException {
150    Objects.requireNonNull(k, "key cannot be null");
151    Objects.requireNonNull(v, "value cannot be null");
152
153    if (checkPrefix && isReservedFileInfoKey(k)) {
154      throw new IOException("Keys with a " + HFileInfo.RESERVED_PREFIX + " are reserved");
155    }
156    put(k, v);
157    return this;
158  }
159
160  /** Return true if the given file info key is reserved for internal use. */
161  public static boolean isReservedFileInfoKey(byte[] key) {
162    return Bytes.startsWith(key, HFileInfo.RESERVED_PREFIX_BYTES);
163  }
164
165  @Override
166  public void clear() {
167    this.map.clear();
168  }
169
170  @Override
171  public Comparator<? super byte[]> comparator() {
172    return map.comparator();
173  }
174
175  @Override
176  public boolean containsKey(Object key) {
177    return map.containsKey(key);
178  }
179
180  @Override
181  public boolean containsValue(Object value) {
182    return map.containsValue(value);
183  }
184
185  @Override
186  public Set<java.util.Map.Entry<byte[], byte[]>> entrySet() {
187    return map.entrySet();
188  }
189
190  @Override
191  public boolean equals(Object o) {
192    return map.equals(o);
193  }
194
195  @Override
196  public byte[] firstKey() {
197    return map.firstKey();
198  }
199
200  @Override
201  public byte[] get(Object key) {
202    return map.get(key);
203  }
204
205  @Override
206  public int hashCode() {
207    return map.hashCode();
208  }
209
210  @Override
211  public SortedMap<byte[], byte[]> headMap(byte[] toKey) {
212    return this.map.headMap(toKey);
213  }
214
215  @Override
216  public boolean isEmpty() {
217    return map.isEmpty();
218  }
219
220  @Override
221  public Set<byte[]> keySet() {
222    return map.keySet();
223  }
224
225  @Override
226  public byte[] lastKey() {
227    return map.lastKey();
228  }
229
230  @Override
231  public byte[] put(byte[] key, byte[] value) {
232    return this.map.put(key, value);
233  }
234
235  @Override
236  public void putAll(Map<? extends byte[], ? extends byte[]> m) {
237    this.map.putAll(m);
238  }
239
240  @Override
241  public byte[] remove(Object key) {
242    return this.map.remove(key);
243  }
244
245  @Override
246  public int size() {
247    return map.size();
248  }
249
250  @Override
251  public SortedMap<byte[], byte[]> subMap(byte[] fromKey, byte[] toKey) {
252    return this.map.subMap(fromKey, toKey);
253  }
254
255  @Override
256  public SortedMap<byte[], byte[]> tailMap(byte[] fromKey) {
257    return this.map.tailMap(fromKey);
258  }
259
260  @Override
261  public Collection<byte[]> values() {
262    return map.values();
263  }
264
265  /**
266   * Write out this instance on the passed in <code>out</code> stream. We write it as a protobuf.
267   * @see #read(DataInputStream)
268   */
269  void write(final DataOutputStream out) throws IOException {
270    HFileProtos.FileInfoProto.Builder builder = HFileProtos.FileInfoProto.newBuilder();
271    for (Map.Entry<byte[], byte[]> e : this.map.entrySet()) {
272      HBaseProtos.BytesBytesPair.Builder bbpBuilder = HBaseProtos.BytesBytesPair.newBuilder();
273      bbpBuilder.setFirst(UnsafeByteOperations.unsafeWrap(e.getKey()));
274      bbpBuilder.setSecond(UnsafeByteOperations.unsafeWrap(e.getValue()));
275      builder.addMapEntry(bbpBuilder.build());
276    }
277    out.write(ProtobufMagic.PB_MAGIC);
278    builder.build().writeDelimitedTo(out);
279  }
280
281  /**
282   * Populate this instance with what we find on the passed in <code>in</code> stream. Can
283   * deserialize protobuf of old Writables format.
284   * @see #write(DataOutputStream)
285   */
286  void read(final DataInputStream in) throws IOException {
287    // This code is tested over in TestHFileReaderV1 where we read an old hfile w/ this new code.
288    int pblen = ProtobufUtil.lengthOfPBMagic();
289    byte[] pbuf = new byte[pblen];
290    if (in.markSupported()) {
291      in.mark(pblen);
292    }
293    int read = in.read(pbuf);
294    if (read != pblen) {
295      throw new IOException("read=" + read + ", wanted=" + pblen);
296    }
297    if (ProtobufUtil.isPBMagicPrefix(pbuf)) {
298      parsePB(HFileProtos.FileInfoProto.parseDelimitedFrom(in));
299    } else {
300      if (in.markSupported()) {
301        in.reset();
302        parseWritable(in);
303      } else {
304        // We cannot use BufferedInputStream, it consumes more than we read from the underlying IS
305        ByteArrayInputStream bais = new ByteArrayInputStream(pbuf);
306        SequenceInputStream sis = new SequenceInputStream(bais, in); // Concatenate input streams
307        // TODO: Am I leaking anything here wrapping the passed in stream? We are not calling
308        // close on the wrapped streams but they should be let go after we leave this context?
309        // I see that we keep a reference to the passed in inputstream but since we no longer
310        // have a reference to this after we leave, we should be ok.
311        parseWritable(new DataInputStream(sis));
312      }
313    }
314  }
315
316  /**
317   * Now parse the old Writable format. It was a list of Map entries. Each map entry was a key and a
318   * value of a byte []. The old map format had a byte before each entry that held a code which was
319   * short for the key or value type. We know it was a byte [] so in below we just read and dump it.
320   */
321  void parseWritable(final DataInputStream in) throws IOException {
322    // First clear the map.
323    // Otherwise we will just accumulate entries every time this method is called.
324    this.map.clear();
325    // Read the number of entries in the map
326    int entries = in.readInt();
327    // Then read each key/value pair
328    for (int i = 0; i < entries; i++) {
329      byte[] key = Bytes.readByteArray(in);
330      // We used to read a byte that encoded the class type.
331      // Read and ignore it because it is always byte [] in hfile
332      in.readByte();
333      byte[] value = Bytes.readByteArray(in);
334      this.map.put(key, value);
335    }
336  }
337
338  /**
339   * Fill our map with content of the pb we read off disk
340   * @param fip protobuf message to read
341   */
342  void parsePB(final HFileProtos.FileInfoProto fip) {
343    this.map.clear();
344    for (BytesBytesPair pair : fip.getMapEntryList()) {
345      this.map.put(pair.getFirst().toByteArray(), pair.getSecond().toByteArray());
346    }
347  }
348
349  public void initTrailerAndContext(ReaderContext context, Configuration conf) throws IOException {
350    try {
351      boolean isHBaseChecksum = context.getInputStreamWrapper().shouldUseHBaseChecksum();
352      trailer = FixedFileTrailer.readFromStream(
353        context.getInputStreamWrapper().getStream(isHBaseChecksum), context.getFileSize());
354      Path path = context.getFilePath();
355      checkFileVersion(path);
356      this.hfileContext = createHFileContext(path, trailer, conf);
357      context.getInputStreamWrapper().unbuffer();
358    } catch (Throwable t) {
359      IOUtils.closeQuietly(context.getInputStreamWrapper(),
360        e -> LOG.warn("failed to close input stream wrapper", e));
361      throw new CorruptHFileException(
362        "Problem reading HFile Trailer from file " + context.getFilePath(), t);
363    }
364  }
365
366  /**
367   * should be called after initTrailerAndContext
368   */
369  public void initMetaAndIndex(HFile.Reader reader) throws IOException {
370    if (initialized) {
371      return;
372    }
373
374    ReaderContext context = reader.getContext();
375    try {
376      HFileBlock.FSReader blockReader = reader.getUncachedBlockReader();
377      // Initialize an block iterator, and parse load-on-open blocks in the following.
378      blockIter = blockReader.blockRange(trailer.getLoadOnOpenDataOffset(),
379        context.getFileSize() - trailer.getTrailerSize());
380      // Data index. We also read statistics about the block index written after
381      // the root level.
382      HFileBlock dataBlockRootIndex = blockIter.nextBlockWithBlockType(BlockType.ROOT_INDEX);
383      HFileBlock metaBlockIndex = blockIter.nextBlockWithBlockType(BlockType.ROOT_INDEX);
384      loadMetaInfo(blockIter, hfileContext);
385
386      HFileIndexBlockEncoder indexBlockEncoder =
387        HFileIndexBlockEncoderImpl.createFromFileInfo(this);
388      this.dataIndexReader = new HFileBlockIndex.CellBasedKeyBlockIndexReaderV2(
389        trailer.createComparator(), trailer.getNumDataIndexLevels(), indexBlockEncoder);
390      dataIndexReader.readMultiLevelIndexRoot(dataBlockRootIndex, trailer.getDataIndexCount());
391      reader.setDataBlockIndexReader(dataIndexReader);
392      // Meta index.
393      this.metaIndexReader = new HFileBlockIndex.ByteArrayKeyBlockIndexReader(1);
394      metaIndexReader.readRootIndex(metaBlockIndex, trailer.getMetaIndexCount());
395      reader.setMetaBlockIndexReader(metaIndexReader);
396
397      reader.setDataBlockEncoder(HFileDataBlockEncoderImpl.createFromFileInfo(this));
398      // Load-On-Open info
399      HFileBlock b;
400      while ((b = blockIter.nextBlock()) != null) {
401        loadOnOpenBlocks.add(b);
402      }
403      // close the block reader
404      context.getInputStreamWrapper().unbuffer();
405
406      put(FILE_SIZE, Bytes.toBytes(context.getFileSize()));
407      put(FILE_PATH, Bytes.toBytes(context.getFilePath().toString()));
408    } catch (Throwable t) {
409      IOUtils.closeQuietly(context.getInputStreamWrapper(),
410        e -> LOG.warn("failed to close input stream wrapper", e));
411      throw new CorruptHFileException(
412        "Problem reading data index and meta index from file " + context.getFilePath(), t);
413    }
414    initialized = true;
415  }
416
417  private HFileContext createHFileContext(Path path, FixedFileTrailer trailer, Configuration conf)
418    throws IOException {
419    HFileContextBuilder builder = new HFileContextBuilder().withHBaseCheckSum(true)
420      .withHFileName(path.getName()).withCompression(trailer.getCompressionCodec())
421      .withDecompressionContext(
422        trailer.getCompressionCodec().getHFileDecompressionContextForConfiguration(conf))
423      .withCellComparator(FixedFileTrailer.createComparator(trailer.getComparatorClassName()));
424    // Check for any key material available
425    byte[] keyBytes = trailer.getEncryptionKey();
426    if (keyBytes != null) {
427      Encryption.Context cryptoContext = Encryption.newContext(conf);
428      Key key = EncryptionUtil.unwrapKey(conf, keyBytes);
429      // Use the algorithm the key wants
430      Cipher cipher = Encryption.getCipher(conf, key.getAlgorithm());
431      if (cipher == null) {
432        throw new IOException(
433          "Cipher '" + key.getAlgorithm() + "' is not available" + ", path=" + path);
434      }
435      cryptoContext.setCipher(cipher);
436      cryptoContext.setKey(key);
437      builder.withEncryptionContext(cryptoContext);
438    }
439    HFileContext context = builder.build();
440    return context;
441  }
442
443  private void loadMetaInfo(HFileBlock.BlockIterator blockIter, HFileContext hfileContext)
444    throws IOException {
445    read(blockIter.nextBlockWithBlockType(BlockType.FILE_INFO).getByteStream());
446    byte[] creationTimeBytes = get(HFileInfo.CREATE_TIME_TS);
447    hfileContext.setFileCreateTime(creationTimeBytes == null ? 0 : Bytes.toLong(creationTimeBytes));
448    byte[] tmp = get(HFileInfo.MAX_TAGS_LEN);
449    // max tag length is not present in the HFile means tags were not at all written to file.
450    if (tmp != null) {
451      hfileContext.setIncludesTags(true);
452      tmp = get(HFileInfo.TAGS_COMPRESSED);
453      if (tmp != null && Bytes.toBoolean(tmp)) {
454        hfileContext.setCompressTags(true);
455      }
456    }
457    // parse meta info
458    if (get(HFileInfo.LASTKEY) != null) {
459      lastKeyCell = new KeyValue.KeyOnlyKeyValue(get(HFileInfo.LASTKEY));
460    }
461    if (get(HFileInfo.KEY_OF_BIGGEST_CELL) != null) {
462      biggestCell = new KeyValue.KeyOnlyKeyValue(get(HFileInfo.KEY_OF_BIGGEST_CELL));
463      lenOfBiggestCell = Bytes.toLong(get(HFileInfo.LEN_OF_BIGGEST_CELL));
464    }
465    avgKeyLen = Bytes.toInt(get(HFileInfo.AVG_KEY_LEN));
466    avgValueLen = Bytes.toInt(get(HFileInfo.AVG_VALUE_LEN));
467    byte[] keyValueFormatVersion = get(HFileWriterImpl.KEY_VALUE_VERSION);
468    includesMemstoreTS = keyValueFormatVersion != null
469      && Bytes.toInt(keyValueFormatVersion) == HFileWriterImpl.KEY_VALUE_VER_WITH_MEMSTORE;
470    hfileContext.setIncludesMvcc(includesMemstoreTS);
471    if (includesMemstoreTS) {
472      decodeMemstoreTS = Bytes.toLong(get(HFileWriterImpl.MAX_MEMSTORE_TS_KEY)) > 0;
473    }
474  }
475
476  /**
477   * File version check is a little sloppy. We read v3 files but can also read v2 files if their
478   * content has been pb'd; files written with 0.98.
479   */
480  private void checkFileVersion(Path path) {
481    int majorVersion = trailer.getMajorVersion();
482    if (majorVersion == getMajorVersion()) {
483      return;
484    }
485    int minorVersion = trailer.getMinorVersion();
486    if (majorVersion == 2 && minorVersion >= MIN_V2_MINOR_VERSION_WITH_PB) {
487      return;
488    }
489    // We can read v3 or v2 versions of hfile.
490    throw new IllegalArgumentException("Invalid HFile version: major=" + trailer.getMajorVersion()
491      + ", minor=" + trailer.getMinorVersion() + ": expected at least " + "major=2 and minor="
492      + MAX_MINOR_VERSION + ", path=" + path);
493  }
494
495  public void close() {
496    if (blockIter != null) {
497      blockIter.freeBlocks();
498    }
499  }
500
501  public int getMajorVersion() {
502    return 3;
503  }
504
505  public void setTrailer(FixedFileTrailer trailer) {
506    this.trailer = trailer;
507  }
508
509  public FixedFileTrailer getTrailer() {
510    return this.trailer;
511  }
512
513  public HFileBlockIndex.CellBasedKeyBlockIndexReader getDataBlockIndexReader() {
514    return this.dataIndexReader;
515  }
516
517  public HFileBlockIndex.ByteArrayKeyBlockIndexReader getMetaBlockIndexReader() {
518    return this.metaIndexReader;
519  }
520
521  public HFileContext getHFileContext() {
522    return this.hfileContext;
523  }
524
525  public List<HFileBlock> getLoadOnOpenBlocks() {
526    return loadOnOpenBlocks;
527  }
528
529  public ExtendedCell getLastKeyCell() {
530    return lastKeyCell;
531  }
532
533  public int getAvgKeyLen() {
534    return avgKeyLen;
535  }
536
537  public int getAvgValueLen() {
538    return avgValueLen;
539  }
540
541  public String getKeyOfBiggestCell() {
542    return CellUtil.toString(biggestCell, false);
543  }
544
545  public long getLenOfBiggestCell() {
546    return lenOfBiggestCell;
547  }
548
549  public boolean shouldIncludeMemStoreTS() {
550    return includesMemstoreTS;
551  }
552
553  public boolean isDecodeMemstoreTS() {
554    return decodeMemstoreTS;
555  }
556}