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