View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.ipc;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.DataInput;
22  import java.io.DataInputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.ByteBuffer;
27  
28  import org.apache.commons.io.IOUtils;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.hadoop.conf.Configurable;
32  import org.apache.hadoop.conf.Configuration;
33  import org.apache.hadoop.hbase.CellScanner;
34  import org.apache.hadoop.hbase.codec.Codec;
35  import org.apache.hadoop.hbase.io.ByteBufferOutputStream;
36  import org.apache.hadoop.hbase.io.HeapSize;
37  import org.apache.hadoop.hbase.util.Bytes;
38  import org.apache.hadoop.hbase.util.ClassSize;
39  import org.apache.hadoop.io.compress.CodecPool;
40  import org.apache.hadoop.io.compress.CompressionCodec;
41  import org.apache.hadoop.io.compress.CompressionInputStream;
42  import org.apache.hadoop.io.compress.Compressor;
43  import org.apache.hadoop.io.compress.Decompressor;
44  
45  import com.google.common.base.Preconditions;
46  import com.google.protobuf.CodedInputStream;
47  import com.google.protobuf.CodedOutputStream;
48  import com.google.protobuf.Message;
49  
50  /**
51   * Utility to help ipc'ing.
52   */
53  class IPCUtil {
54    public static final Log LOG = LogFactory.getLog(IPCUtil.class);
55    /**
56     * How much we think the decompressor will expand the original compressed content.
57     */
58    private final int cellBlockDecompressionMultiplier;
59    private final int cellBlockBuildingInitialBufferSize;
60    private final Configuration conf;
61  
62    IPCUtil(final Configuration conf) {
63      super();
64      this.conf = conf;
65      this.cellBlockDecompressionMultiplier =
66          conf.getInt("hbase.ipc.cellblock.decompression.buffersize.multiplier", 3);
67      // Guess that 16k is a good size for rpc buffer.  Could go bigger.  See the TODO below in
68      // #buildCellBlock.
69      this.cellBlockBuildingInitialBufferSize =
70        ClassSize.align(conf.getInt("hbase.ipc.cellblock.building.initial.buffersize", 16 * 1024));
71    }
72  
73    /**
74     * Puts CellScanner Cells into a cell block using passed in <code>codec</code> and/or
75     * <code>compressor</code>.
76     * @param codec
77     * @param compressor
78     * @Param cellScanner
79     * @return Null or byte buffer filled with a cellblock filled with passed-in Cells encoded using
80     * passed in <code>codec</code> and/or <code>compressor</code>; the returned buffer has been
81     * flipped and is ready for reading.  Use limit to find total size.
82     * @throws IOException
83     */
84    @SuppressWarnings("resource")
85    ByteBuffer buildCellBlock(final Codec codec, final CompressionCodec compressor,
86      final CellScanner cellScanner)
87    throws IOException {
88      if (cellScanner == null) return null;
89      int bufferSize = this.cellBlockBuildingInitialBufferSize;
90      if (cellScanner instanceof HeapSize) {
91        long longSize = ((HeapSize)cellScanner).heapSize();
92        // Just make sure we don't have a size bigger than an int.
93        if (longSize > Integer.MAX_VALUE) {
94          throw new IOException("Size " + longSize + " > " + Integer.MAX_VALUE);
95        }
96        bufferSize = ClassSize.align((int)longSize);
97      } // TODO: Else, get estimate on size of buffer rather than have the buffer resize.
98      // See TestIPCUtil main for experiment where we spin through the Cells getting estimate of
99      // total size before creating the buffer.  It costs somw small percentage.  If we are usually
100     // within the estimated buffer size, then the cost is not worth it.  If we are often well
101     // outside the guesstimated buffer size, the processing can be done in half the time if we
102     // go w/ the estimated size rather than let the buffer resize.
103     ByteBufferOutputStream baos = new ByteBufferOutputStream(bufferSize);
104     OutputStream os = baos;
105     Compressor poolCompressor = null;
106     try {
107       if (compressor != null) {
108         if (compressor instanceof Configurable) ((Configurable)compressor).setConf(this.conf);
109         poolCompressor = CodecPool.getCompressor(compressor);
110         os = compressor.createOutputStream(os, poolCompressor);
111       }
112       Codec.Encoder encoder = codec.getEncoder(os);
113       while (cellScanner.advance()) {
114         encoder.write(cellScanner.current());
115       }
116       encoder.flush();
117     } finally {
118       os.close();
119       if (poolCompressor != null) CodecPool.returnCompressor(poolCompressor);
120     }
121     if (LOG.isTraceEnabled()) {
122       if (bufferSize < baos.size()) {
123         LOG.trace("Buffer grew from initial bufferSize=" + bufferSize + " to " + baos.size() +
124           "; up hbase.ipc.cellblock.building.initial.buffersize?");
125       }
126     }
127     return baos.getByteBuffer();
128   }
129 
130   /**
131    * @param codec
132    * @param cellBlock
133    * @return CellScanner to work against the content of <code>cellBlock</code>
134    * @throws IOException
135    */
136   CellScanner createCellScanner(final Codec codec, final CompressionCodec compressor,
137       final byte [] cellBlock)
138   throws IOException {
139     return createCellScanner(codec, compressor, cellBlock, 0, cellBlock.length);
140   }
141 
142   /**
143    * @param codec
144    * @param cellBlock
145    * @param offset
146    * @param length
147    * @return CellScanner to work against the content of <code>cellBlock</code>
148    * @throws IOException
149    */
150   CellScanner createCellScanner(final Codec codec, final CompressionCodec compressor,
151       final byte [] cellBlock, final int offset, final int length)
152   throws IOException {
153     // If compressed, decompress it first before passing it on else we will leak compression
154     // resources if the stream is not closed properly after we let it out.
155     InputStream is = null;
156     if (compressor != null) {
157       // GZIPCodec fails w/ NPE if no configuration.
158       if (compressor instanceof Configurable) ((Configurable)compressor).setConf(this.conf);
159       Decompressor poolDecompressor = CodecPool.getDecompressor(compressor);
160       CompressionInputStream cis =
161         compressor.createInputStream(new ByteArrayInputStream(cellBlock, offset, length),
162         poolDecompressor);
163       try {
164         // TODO: This is ugly.  The buffer will be resized on us if we guess wrong.
165         // TODO: Reuse buffers.
166         ByteBufferOutputStream bbos = new ByteBufferOutputStream((length - offset) *
167           this.cellBlockDecompressionMultiplier);
168         IOUtils.copy(cis, bbos);
169         bbos.close();
170         ByteBuffer bb = bbos.getByteBuffer();
171         is = new ByteArrayInputStream(bb.array(), 0, bb.limit());
172       } finally {
173         if (is != null) is.close();
174         CodecPool.returnDecompressor(poolDecompressor);
175       }
176     } else {
177       is = new ByteArrayInputStream(cellBlock, offset, length);
178     }
179     return codec.getDecoder(is);
180   }
181 
182   /**
183    * Write out header, param, and cell block if there to a {@link ByteBufferOutputStream} sized
184    * to hold these elements.
185    * @param header
186    * @param param
187    * @param cellBlock
188    * @return A {@link ByteBufferOutputStream} filled with the content of the passed in
189    * <code>header</code>, <code>param</code>, and <code>cellBlock</code>.
190    * @throws IOException
191    */
192   static ByteBufferOutputStream write(final Message header, final Message param,
193       final ByteBuffer cellBlock)
194   throws IOException {
195     int totalSize = getTotalSizeWhenWrittenDelimited(header, param);
196     if (cellBlock != null) totalSize += cellBlock.limit();
197     ByteBufferOutputStream bbos = new ByteBufferOutputStream(totalSize);
198     write(bbos, header, param, cellBlock, totalSize);
199     bbos.close();
200     return bbos;
201   }
202 
203   /**
204    * Write out header, param, and cell block if there is one.
205    * @param dos
206    * @param header
207    * @param param
208    * @param cellBlock
209    * @return Total number of bytes written.
210    * @throws IOException
211    */
212   static int write(final OutputStream dos, final Message header, final Message param,
213       final ByteBuffer cellBlock)
214   throws IOException {
215     // Must calculate total size and write that first so other side can read it all in in one
216     // swoop.  This is dictated by how the server is currently written.  Server needs to change
217     // if we are to be able to write without the length prefixing.
218     int totalSize = IPCUtil.getTotalSizeWhenWrittenDelimited(header, param);
219     if (cellBlock != null) totalSize += cellBlock.remaining();
220     return write(dos, header, param, cellBlock, totalSize);
221   }
222 
223   private static int write(final OutputStream dos, final Message header, final Message param,
224     final ByteBuffer cellBlock, final int totalSize)
225   throws IOException {
226     // I confirmed toBytes does same as say DataOutputStream#writeInt.
227     dos.write(Bytes.toBytes(totalSize));
228     header.writeDelimitedTo(dos);
229     if (param != null) param.writeDelimitedTo(dos);
230     if (cellBlock != null) dos.write(cellBlock.array(), 0, cellBlock.remaining());
231     dos.flush();
232     return totalSize;
233   }
234 
235   /**
236    * @param in Stream cue'd up just before a delimited message
237    * @return Bytes that hold the bytes that make up the message read from <code>in</code>
238    * @throws IOException
239    */
240   static byte [] getDelimitedMessageBytes(final DataInputStream in) throws IOException {
241     byte b = in.readByte();
242     int size = CodedInputStream.readRawVarint32(b, in);
243     // Allocate right-sized buffer rather than let pb allocate its default minimum 4k.
244     byte [] bytes = new byte[size];
245     IOUtils.readFully(in, bytes);
246     return bytes;
247   }
248 
249   /**
250    * Read in chunks of 8K (HBASE-7239)
251    * @param in
252    * @param dest
253    * @param offset
254    * @param len
255    * @throws IOException
256    */
257   static void readChunked(final DataInput in, byte[] dest, int offset, int len)
258       throws IOException {
259     int maxRead = 8192;
260 
261     for (; offset < len; offset += maxRead) {
262       in.readFully(dest, offset, Math.min(len - offset, maxRead));
263     }
264   }
265 
266   /**
267    * @param header
268    * @param body
269    * @return Size on the wire when the two messages are written with writeDelimitedTo
270    */
271   static int getTotalSizeWhenWrittenDelimited(Message ... messages) {
272     int totalSize = 0;
273     for (Message m: messages) {
274       if (m == null) continue;
275       totalSize += m.getSerializedSize();
276       totalSize += CodedOutputStream.computeRawVarint32Size(m.getSerializedSize());
277     }
278     Preconditions.checkArgument(totalSize < Integer.MAX_VALUE);
279     return totalSize;
280   }
281 }