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}