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.regionserver.wal;
019
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.hbase.Cell;
027import org.apache.hadoop.hbase.CellUtil;
028import org.apache.hadoop.hbase.HBaseInterfaceAudience;
029import org.apache.hadoop.hbase.PrivateCellUtil;
030import org.apache.hadoop.hbase.KeyValue;
031import org.apache.hadoop.hbase.KeyValueUtil;
032import org.apache.yetus.audience.InterfaceAudience;
033import org.apache.hadoop.hbase.codec.BaseDecoder;
034import org.apache.hadoop.hbase.codec.BaseEncoder;
035import org.apache.hadoop.hbase.codec.Codec;
036import org.apache.hadoop.hbase.codec.KeyValueCodecWithTags;
037import org.apache.hadoop.hbase.io.ByteBuffInputStream;
038import org.apache.hadoop.hbase.io.ByteBufferWriter;
039import org.apache.hadoop.hbase.io.ByteBufferWriterOutputStream;
040import org.apache.hadoop.hbase.io.util.Dictionary;
041import org.apache.hadoop.hbase.io.util.StreamUtils;
042import org.apache.hadoop.hbase.nio.ByteBuff;
043import org.apache.hadoop.hbase.util.ByteBufferUtils;
044import org.apache.hadoop.hbase.util.Bytes;
045import org.apache.hadoop.hbase.util.ReflectionUtils;
046import org.apache.hadoop.io.IOUtils;
047
048import org.apache.hbase.thirdparty.com.google.protobuf.ByteString;
049
050
051/**
052 * Compression in this class is lifted off Compressor/KeyValueCompression.
053 * This is a pure coincidence... they are independent and don't have to be compatible.
054 *
055 * This codec is used at server side for writing cells to WAL as well as for sending edits
056 * as part of the distributed splitting process.
057 */
058@InterfaceAudience.LimitedPrivate({HBaseInterfaceAudience.COPROC,
059  HBaseInterfaceAudience.PHOENIX, HBaseInterfaceAudience.CONFIG})
060public class WALCellCodec implements Codec {
061  /** Configuration key for the class to use when encoding cells in the WAL */
062  public static final String WAL_CELL_CODEC_CLASS_KEY = "hbase.regionserver.wal.codec";
063
064  protected final CompressionContext compression;
065  protected final ByteStringUncompressor statelessUncompressor = new ByteStringUncompressor() {
066    @Override
067    public byte[] uncompress(ByteString data, Dictionary dict) throws IOException {
068      return WALCellCodec.uncompressByteString(data, dict);
069    }
070  };
071
072  /**
073   * <b>All subclasses must implement a no argument constructor</b>
074   */
075  public WALCellCodec() {
076    this.compression = null;
077  }
078
079  /**
080   * Default constructor - <b>all subclasses must implement a constructor with this signature </b>
081   * if they are to be dynamically loaded from the {@link Configuration}.
082   * @param conf configuration to configure <tt>this</tt>
083   * @param compression compression the codec should support, can be <tt>null</tt> to indicate no
084   *          compression
085   */
086  public WALCellCodec(Configuration conf, CompressionContext compression) {
087    this.compression = compression;
088  }
089
090  public static String getWALCellCodecClass(Configuration conf) {
091    return conf.get(WAL_CELL_CODEC_CLASS_KEY, WALCellCodec.class.getName());
092  }
093
094  /**
095   * Create and setup a {@link WALCellCodec} from the {@code cellCodecClsName} and
096   * CompressionContext, if {@code cellCodecClsName} is specified.
097   * Otherwise Cell Codec classname is read from {@link Configuration}.
098   * Fully prepares the codec for use.
099   * @param conf {@link Configuration} to read for the user-specified codec. If none is specified,
100   *          uses a {@link WALCellCodec}.
101   * @param cellCodecClsName name of codec
102   * @param compression compression the codec should use
103   * @return a {@link WALCellCodec} ready for use.
104   * @throws UnsupportedOperationException if the codec cannot be instantiated
105   */
106
107  public static WALCellCodec create(Configuration conf, String cellCodecClsName,
108      CompressionContext compression) throws UnsupportedOperationException {
109    if (cellCodecClsName == null) {
110      cellCodecClsName = getWALCellCodecClass(conf);
111    }
112    return ReflectionUtils.instantiateWithCustomCtor(cellCodecClsName, new Class[]
113        { Configuration.class, CompressionContext.class }, new Object[] { conf, compression });
114  }
115
116  /**
117   * Create and setup a {@link WALCellCodec} from the
118   * CompressionContext.
119   * Cell Codec classname is read from {@link Configuration}.
120   * Fully prepares the codec for use.
121   * @param conf {@link Configuration} to read for the user-specified codec. If none is specified,
122   *          uses a {@link WALCellCodec}.
123   * @param compression compression the codec should use
124   * @return a {@link WALCellCodec} ready for use.
125   * @throws UnsupportedOperationException if the codec cannot be instantiated
126   */
127  public static WALCellCodec create(Configuration conf,
128      CompressionContext compression) throws UnsupportedOperationException {
129    String cellCodecClsName = getWALCellCodecClass(conf);
130    return ReflectionUtils.instantiateWithCustomCtor(cellCodecClsName, new Class[]
131        { Configuration.class, CompressionContext.class }, new Object[] { conf, compression });
132  }
133
134  public interface ByteStringCompressor {
135    ByteString compress(byte[] data, Dictionary dict) throws IOException;
136  }
137
138  public interface ByteStringUncompressor {
139    byte[] uncompress(ByteString data, Dictionary dict) throws IOException;
140  }
141
142  // TODO: it sucks that compression context is in WAL.Entry. It'd be nice if it was here.
143  //       Dictionary could be gotten by enum; initially, based on enum, context would create
144  //       an array of dictionaries.
145  static class BaosAndCompressor extends ByteArrayOutputStream implements ByteStringCompressor {
146    public ByteString toByteString() {
147      // We need this copy to create the ByteString as the byte[] 'buf' is not immutable. We reuse
148      // them.
149      return ByteString.copyFrom(this.buf, 0, this.count);
150    }
151
152    @Override
153    public ByteString compress(byte[] data, Dictionary dict) throws IOException {
154      writeCompressed(data, dict);
155      // We need this copy to create the ByteString as the byte[] 'buf' is not immutable. We reuse
156      // them.
157      ByteString result = ByteString.copyFrom(this.buf, 0, this.count);
158      reset(); // Only resets the count - we reuse the byte array.
159      return result;
160    }
161
162    private void writeCompressed(byte[] data, Dictionary dict) throws IOException {
163      assert dict != null;
164      short dictIdx = dict.findEntry(data, 0, data.length);
165      if (dictIdx == Dictionary.NOT_IN_DICTIONARY) {
166        write(Dictionary.NOT_IN_DICTIONARY);
167        StreamUtils.writeRawVInt32(this, data.length);
168        write(data, 0, data.length);
169      } else {
170        StreamUtils.writeShort(this, dictIdx);
171      }
172    }
173  }
174
175  private static byte[] uncompressByteString(ByteString bs, Dictionary dict) throws IOException {
176    InputStream in = bs.newInput();
177    byte status = (byte)in.read();
178    if (status == Dictionary.NOT_IN_DICTIONARY) {
179      byte[] arr = new byte[StreamUtils.readRawVarint32(in)];
180      int bytesRead = in.read(arr);
181      if (bytesRead != arr.length) {
182        throw new IOException("Cannot read; wanted " + arr.length + ", but got " + bytesRead);
183      }
184      if (dict != null) dict.addEntry(arr, 0, arr.length);
185      return arr;
186    } else {
187      // Status here is the higher-order byte of index of the dictionary entry.
188      short dictIdx = StreamUtils.toShort(status, (byte)in.read());
189      byte[] entry = dict.getEntry(dictIdx);
190      if (entry == null) {
191        throw new IOException("Missing dictionary entry for index " + dictIdx);
192      }
193      return entry;
194    }
195  }
196
197  static class CompressedKvEncoder extends BaseEncoder {
198    private final CompressionContext compression;
199    public CompressedKvEncoder(OutputStream out, CompressionContext compression) {
200      super(out);
201      this.compression = compression;
202    }
203
204    @Override
205    public void write(Cell cell) throws IOException {
206      // We first write the KeyValue infrastructure as VInts.
207      StreamUtils.writeRawVInt32(out, KeyValueUtil.keyLength(cell));
208      StreamUtils.writeRawVInt32(out, cell.getValueLength());
209      // To support tags
210      int tagsLength = cell.getTagsLength();
211      StreamUtils.writeRawVInt32(out, tagsLength);
212      PrivateCellUtil.compressRow(out, cell, compression.rowDict);
213      PrivateCellUtil.compressFamily(out, cell, compression.familyDict);
214      PrivateCellUtil.compressQualifier(out, cell, compression.qualifierDict);
215      // Write timestamp, type and value as uncompressed.
216      StreamUtils.writeLong(out, cell.getTimestamp());
217      out.write(cell.getTypeByte());
218      PrivateCellUtil.writeValue(out, cell, cell.getValueLength());
219      if (tagsLength > 0) {
220        if (compression.tagCompressionContext != null) {
221          // Write tags using Dictionary compression
222          PrivateCellUtil.compressTags(out, cell, compression.tagCompressionContext);
223        } else {
224          // Tag compression is disabled within the WAL compression. Just write the tags bytes as
225          // it is.
226          PrivateCellUtil.writeTags(out, cell, tagsLength);
227        }
228      }
229    }
230  }
231
232  static class CompressedKvDecoder extends BaseDecoder {
233    private final CompressionContext compression;
234    public CompressedKvDecoder(InputStream in, CompressionContext compression) {
235      super(in);
236      this.compression = compression;
237    }
238
239    @Override
240    protected Cell parseCell() throws IOException {
241      int keylength = StreamUtils.readRawVarint32(in);
242      int vlength = StreamUtils.readRawVarint32(in);
243
244      int tagsLength = StreamUtils.readRawVarint32(in);
245      int length = 0;
246      if(tagsLength == 0) {
247        length = KeyValue.KEYVALUE_INFRASTRUCTURE_SIZE + keylength + vlength;
248      } else {
249        length = KeyValue.KEYVALUE_WITH_TAGS_INFRASTRUCTURE_SIZE + keylength + vlength + tagsLength;
250      }
251
252      byte[] backingArray = new byte[length];
253      int pos = 0;
254      pos = Bytes.putInt(backingArray, pos, keylength);
255      pos = Bytes.putInt(backingArray, pos, vlength);
256
257      // the row
258      int elemLen = readIntoArray(backingArray, pos + Bytes.SIZEOF_SHORT, compression.rowDict);
259      checkLength(elemLen, Short.MAX_VALUE);
260      pos = Bytes.putShort(backingArray, pos, (short)elemLen);
261      pos += elemLen;
262
263      // family
264      elemLen = readIntoArray(backingArray, pos + Bytes.SIZEOF_BYTE, compression.familyDict);
265      checkLength(elemLen, Byte.MAX_VALUE);
266      pos = Bytes.putByte(backingArray, pos, (byte)elemLen);
267      pos += elemLen;
268
269      // qualifier
270      elemLen = readIntoArray(backingArray, pos, compression.qualifierDict);
271      pos += elemLen;
272
273      // timestamp, type and value
274      int tsTypeValLen = length - pos;
275      if (tagsLength > 0) {
276        tsTypeValLen = tsTypeValLen - tagsLength - KeyValue.TAGS_LENGTH_SIZE;
277      }
278      IOUtils.readFully(in, backingArray, pos, tsTypeValLen);
279      pos += tsTypeValLen;
280
281      // tags
282      if (tagsLength > 0) {
283        pos = Bytes.putAsShort(backingArray, pos, tagsLength);
284        if (compression.tagCompressionContext != null) {
285          compression.tagCompressionContext.uncompressTags(in, backingArray, pos, tagsLength);
286        } else {
287          IOUtils.readFully(in, backingArray, pos, tagsLength);
288        }
289      }
290      return new KeyValue(backingArray, 0, length);
291    }
292
293    private int readIntoArray(byte[] to, int offset, Dictionary dict) throws IOException {
294      byte status = (byte)in.read();
295      if (status == Dictionary.NOT_IN_DICTIONARY) {
296        // status byte indicating that data to be read is not in dictionary.
297        // if this isn't in the dictionary, we need to add to the dictionary.
298        int length = StreamUtils.readRawVarint32(in);
299        IOUtils.readFully(in, to, offset, length);
300        dict.addEntry(to, offset, length);
301        return length;
302      } else {
303        // the status byte also acts as the higher order byte of the dictionary entry.
304        short dictIdx = StreamUtils.toShort(status, (byte)in.read());
305        byte[] entry = dict.getEntry(dictIdx);
306        if (entry == null) {
307          throw new IOException("Missing dictionary entry for index " + dictIdx);
308        }
309        // now we write the uncompressed value.
310        Bytes.putBytes(to, offset, entry, 0, entry.length);
311        return entry.length;
312      }
313    }
314
315    private static void checkLength(int len, int max) throws IOException {
316      if (len < 0 || len > max) {
317        throw new IOException("Invalid length for compresesed portion of keyvalue: " + len);
318      }
319    }
320  }
321
322  public static class EnsureKvEncoder extends BaseEncoder {
323    public EnsureKvEncoder(OutputStream out) {
324      super(out);
325    }
326    @Override
327    public void write(Cell cell) throws IOException {
328      checkFlushed();
329      // Make sure to write tags into WAL
330      ByteBufferUtils.putInt(this.out, KeyValueUtil.getSerializedSize(cell, true));
331      KeyValueUtil.oswrite(cell, this.out, true);
332    }
333  }
334
335  @Override
336  public Decoder getDecoder(InputStream is) {
337    return (compression == null)
338        ? new KeyValueCodecWithTags.KeyValueDecoder(is) : new CompressedKvDecoder(is, compression);
339  }
340
341  @Override
342  public Decoder getDecoder(ByteBuff buf) {
343    return getDecoder(new ByteBuffInputStream(buf));
344  }
345
346  @Override
347  public Encoder getEncoder(OutputStream os) {
348    os = (os instanceof ByteBufferWriter) ? os
349        : new ByteBufferWriterOutputStream(os);
350    if (compression == null) {
351      return new EnsureKvEncoder(os);
352    }
353    return new CompressedKvEncoder(os, compression);
354  }
355
356  public ByteStringCompressor getByteStringCompressor() {
357    // TODO: ideally this should also encapsulate compressionContext
358    return new BaosAndCompressor();
359  }
360
361  public ByteStringUncompressor getByteStringUncompressor() {
362    // TODO: ideally this should also encapsulate compressionContext
363    return this.statelessUncompressor;
364  }
365}