View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  package org.apache.hadoop.hbase.regionserver.wal;
21  
22  import java.io.EOFException;
23  import java.io.IOException;
24  import java.nio.ByteBuffer;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.List;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.hadoop.hbase.classification.InterfaceAudience;
32  import org.apache.hadoop.conf.Configuration;
33  import org.apache.hadoop.fs.Path;
34  import org.apache.hadoop.fs.FileSystem;
35  import org.apache.hadoop.fs.FSDataInputStream;
36  import org.apache.hadoop.hbase.codec.Codec;
37  import org.apache.hadoop.hbase.io.LimitInputStream;
38  import org.apache.hadoop.hbase.HBaseInterfaceAudience;
39  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
40  import org.apache.hadoop.hbase.protobuf.generated.WALProtos;
41  import org.apache.hadoop.hbase.protobuf.generated.WALProtos.WALHeader.Builder;
42  import org.apache.hadoop.hbase.protobuf.generated.WALProtos.WALKey;
43  import org.apache.hadoop.hbase.protobuf.generated.WALProtos.WALTrailer;
44  import org.apache.hadoop.hbase.util.Bytes;
45  import org.apache.hadoop.hbase.wal.WAL.Entry;
46  
47  import com.google.protobuf.CodedInputStream;
48  
49  /**
50   * A Protobuf based WAL has the following structure:
51   * <p>
52   * &lt;PB_WAL_MAGIC&gt;&lt;WALHeader&gt;&lt;WALEdits&gt;...&lt;WALEdits&gt;&lt;Trailer&gt;
53   * &lt;TrailerSize&gt; &lt;PB_WAL_COMPLETE_MAGIC&gt;
54   * </p>
55   * The Reader reads meta information (WAL Compression state, WALTrailer, etc) in
56   * ProtobufLogReader#initReader(FSDataInputStream). A WALTrailer is an extensible structure
57   * which is appended at the end of the WAL. This is empty for now; it can contain some meta
58   * information such as Region level stats, etc in future.
59   */
60  @InterfaceAudience.LimitedPrivate({HBaseInterfaceAudience.COPROC, HBaseInterfaceAudience.PHOENIX,
61    HBaseInterfaceAudience.CONFIG})
62  public class ProtobufLogReader extends ReaderBase {
63    private static final Log LOG = LogFactory.getLog(ProtobufLogReader.class);
64    // public for WALFactory until we move everything to o.a.h.h.wal
65    @InterfaceAudience.Private
66    public static final byte[] PB_WAL_MAGIC = Bytes.toBytes("PWAL");
67    // public for TestWALSplit
68    @InterfaceAudience.Private
69    public static final byte[] PB_WAL_COMPLETE_MAGIC = Bytes.toBytes("LAWP");
70    /**
71     * Configuration name of WAL Trailer's warning size. If a waltrailer's size is greater than the
72     * configured size, providers should log a warning. e.g. this is used with Protobuf reader/writer.
73     */
74    static final String WAL_TRAILER_WARN_SIZE = "hbase.regionserver.waltrailer.warn.size";
75    static final int DEFAULT_WAL_TRAILER_WARN_SIZE = 1024 * 1024; // 1MB
76  
77    protected FSDataInputStream inputStream;
78    protected Codec.Decoder cellDecoder;
79    protected WALCellCodec.ByteStringUncompressor byteStringUncompressor;
80    protected boolean hasCompression = false;
81    protected boolean hasTagCompression = false;
82    // walEditsStopOffset is the position of the last byte to read. After reading the last WALEdit
83    // entry in the wal, the inputstream's position is equal to walEditsStopOffset.
84    private long walEditsStopOffset;
85    private boolean trailerPresent;
86    protected WALTrailer trailer;
87    // maximum size of the wal Trailer in bytes. If a user writes/reads a trailer with size larger
88    // than this size, it is written/read respectively, with a WARN message in the log.
89    protected int trailerWarnSize;
90    private static List<String> writerClsNames = new ArrayList<String>();
91    static {
92      writerClsNames.add(ProtobufLogWriter.class.getSimpleName());
93    }
94    
95    // cell codec classname
96    private String codecClsName = null;
97  
98    enum WALHdrResult {
99      EOF,                   // stream is at EOF when method starts
100     SUCCESS,
101     UNKNOWN_WRITER_CLS     // name of writer class isn't recognized
102   }
103   
104   // context for WALHdr carrying information such as Cell Codec classname
105   static class WALHdrContext {
106     WALHdrResult result;
107     String cellCodecClsName;
108     
109     WALHdrContext(WALHdrResult result, String cellCodecClsName) {
110       this.result = result;
111       this.cellCodecClsName = cellCodecClsName;
112     }
113     WALHdrResult getResult() {
114       return result;
115     }
116     String getCellCodecClsName() {
117       return cellCodecClsName;
118     }
119   }
120 
121   public ProtobufLogReader() {
122     super();
123   }
124 
125   @Override
126   public void close() throws IOException {
127     if (this.inputStream != null) {
128       this.inputStream.close();
129       this.inputStream = null;
130     }
131   }
132 
133   @Override
134   public long getPosition() throws IOException {
135     return inputStream.getPos();
136   }
137 
138   @Override
139   public void reset() throws IOException {
140     String clsName = initInternal(null, false);
141     initAfterCompression(clsName); // We need a new decoder (at least).
142   }
143 
144   @Override
145   public void init(FileSystem fs, Path path, Configuration conf, FSDataInputStream stream)
146       throws IOException {
147     this.trailerWarnSize = conf.getInt(WAL_TRAILER_WARN_SIZE, DEFAULT_WAL_TRAILER_WARN_SIZE);
148     super.init(fs, path, conf, stream);
149   }
150 
151   @Override
152   protected String initReader(FSDataInputStream stream) throws IOException {
153     return initInternal(stream, true);
154   }
155 
156   /*
157    * Returns names of the accepted writer classes
158    */
159   public List<String> getWriterClsNames() {
160     return writerClsNames;
161   }
162   
163   /*
164    * Returns the cell codec classname
165    */
166   public String getCodecClsName() {
167       return codecClsName;
168   }
169 
170   protected WALHdrContext readHeader(Builder builder, FSDataInputStream stream)
171       throws IOException {
172      boolean res = builder.mergeDelimitedFrom(stream);
173      if (!res) return new WALHdrContext(WALHdrResult.EOF, null);
174      if (builder.hasWriterClsName() &&
175          !getWriterClsNames().contains(builder.getWriterClsName())) {
176        return new WALHdrContext(WALHdrResult.UNKNOWN_WRITER_CLS, null);
177      }
178      String clsName = null;
179      if (builder.hasCellCodecClsName()) {
180        clsName = builder.getCellCodecClsName();
181      }
182      return new WALHdrContext(WALHdrResult.SUCCESS, clsName);
183   }
184 
185   private String initInternal(FSDataInputStream stream, boolean isFirst)
186       throws IOException {
187     close();
188     long expectedPos = PB_WAL_MAGIC.length;
189     if (stream == null) {
190       stream = fs.open(path);
191       stream.seek(expectedPos);
192     }
193     if (stream.getPos() != expectedPos) {
194       throw new IOException("The stream is at invalid position: " + stream.getPos());
195     }
196     // Initialize metadata or, when we reset, just skip the header.
197     WALProtos.WALHeader.Builder builder = WALProtos.WALHeader.newBuilder();
198     WALHdrContext hdrCtxt = readHeader(builder, stream);
199     WALHdrResult walHdrRes = hdrCtxt.getResult();
200     if (walHdrRes == WALHdrResult.EOF) {
201       throw new EOFException("Couldn't read WAL PB header");
202     }
203     if (walHdrRes == WALHdrResult.UNKNOWN_WRITER_CLS) {
204       throw new IOException("Got unknown writer class: " + builder.getWriterClsName());
205     }
206     if (isFirst) {
207       WALProtos.WALHeader header = builder.build();
208       this.hasCompression = header.hasHasCompression() && header.getHasCompression();
209       this.hasTagCompression = header.hasHasTagCompression() && header.getHasTagCompression();
210     }
211     this.inputStream = stream;
212     this.walEditsStopOffset = this.fileLength;
213     long currentPosition = stream.getPos();
214     trailerPresent = setTrailerIfPresent();
215     this.seekOnFs(currentPosition);
216     if (LOG.isTraceEnabled()) {
217       LOG.trace("After reading the trailer: walEditsStopOffset: " + this.walEditsStopOffset
218           + ", fileLength: " + this.fileLength + ", " + "trailerPresent: " + trailerPresent);
219     }
220     
221     codecClsName = hdrCtxt.getCellCodecClsName();
222     
223     return hdrCtxt.getCellCodecClsName();
224   }
225 
226   /**
227    * To check whether a trailer is present in a WAL, it seeks to position (fileLength -
228    * PB_WAL_COMPLETE_MAGIC.size() - Bytes.SIZEOF_INT). It reads the int value to know the size of
229    * the trailer, and checks whether the trailer is present at the end or not by comparing the last
230    * PB_WAL_COMPLETE_MAGIC.size() bytes. In case trailer is not present, it returns false;
231    * otherwise, sets the trailer and sets this.walEditsStopOffset variable up to the point just
232    * before the trailer.
233    * <ul>
234    * The trailer is ignored in case:
235    * <li>fileLength is 0 or not correct (when file is under recovery, etc).
236    * <li>the trailer size is negative.
237    * </ul>
238    * <p>
239    * In case the trailer size > this.trailerMaxSize, it is read after a WARN message.
240    * @return true if a valid trailer is present
241    * @throws IOException
242    */
243   private boolean setTrailerIfPresent() {
244     try {
245       long trailerSizeOffset = this.fileLength - (PB_WAL_COMPLETE_MAGIC.length + Bytes.SIZEOF_INT);
246       if (trailerSizeOffset <= 0) return false;// no trailer possible.
247       this.seekOnFs(trailerSizeOffset);
248       // read the int as trailer size.
249       int trailerSize = this.inputStream.readInt();
250       ByteBuffer buf = ByteBuffer.allocate(ProtobufLogReader.PB_WAL_COMPLETE_MAGIC.length);
251       this.inputStream.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
252       if (!Arrays.equals(buf.array(), PB_WAL_COMPLETE_MAGIC)) {
253         LOG.trace("No trailer found.");
254         return false;
255       }
256       if (trailerSize < 0) {
257         LOG.warn("Invalid trailer Size " + trailerSize + ", ignoring the trailer");
258         return false;
259       } else if (trailerSize > this.trailerWarnSize) {
260         // continue reading after warning the user.
261         LOG.warn("Please investigate WALTrailer usage. Trailer size > maximum configured size : "
262           + trailerSize + " > " + this.trailerWarnSize);
263       }
264       // seek to the position where trailer starts.
265       long positionOfTrailer = trailerSizeOffset - trailerSize;
266       this.seekOnFs(positionOfTrailer);
267       // read the trailer.
268       buf = ByteBuffer.allocate(trailerSize);// for trailer.
269       this.inputStream.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
270       trailer = WALTrailer.parseFrom(buf.array());
271       this.walEditsStopOffset = positionOfTrailer;
272       return true;
273     } catch (IOException ioe) {
274       LOG.warn("Got IOE while reading the trailer. Continuing as if no trailer is present.", ioe);
275     }
276     return false;
277   }
278 
279   protected WALCellCodec getCodec(Configuration conf, String cellCodecClsName,
280       CompressionContext compressionContext) throws IOException {
281     return WALCellCodec.create(conf, cellCodecClsName, compressionContext);
282   }
283 
284   @Override
285   protected void initAfterCompression() throws IOException {
286     initAfterCompression(null);
287   }
288   
289   @Override
290   protected void initAfterCompression(String cellCodecClsName) throws IOException {
291     WALCellCodec codec = getCodec(this.conf, cellCodecClsName, this.compressionContext);
292     this.cellDecoder = codec.getDecoder(this.inputStream);
293     if (this.hasCompression) {
294       this.byteStringUncompressor = codec.getByteStringUncompressor();
295     }
296   }
297 
298   @Override
299   protected boolean hasCompression() {
300     return this.hasCompression;
301   }
302 
303   @Override
304   protected boolean hasTagCompression() {
305     return this.hasTagCompression;
306   }
307 
308   @Override
309   protected boolean readNext(Entry entry) throws IOException {
310     while (true) {
311       // OriginalPosition might be < 0 on local fs; if so, it is useless to us.
312       long originalPosition = this.inputStream.getPos();
313       if (trailerPresent && originalPosition > 0 && originalPosition == this.walEditsStopOffset) {
314         return false;
315       }
316       WALKey.Builder builder = WALKey.newBuilder();
317       long size = 0;
318       try {
319         long available = -1;
320         try {
321           int firstByte = this.inputStream.read();
322           if (firstByte == -1) {
323             throw new EOFException("First byte is negative");
324           }
325           size = CodedInputStream.readRawVarint32(firstByte, this.inputStream);
326           // available may be < 0 on local fs for instance.  If so, can't depend on it.
327           available = this.inputStream.available();
328           if (available > 0 && available < size) {
329             throw new EOFException("Available stream not enough for edit, " +
330                 "inputStream.available()= " + this.inputStream.available() + ", " +
331                 "entry size= " + size);
332           }
333           ProtobufUtil.mergeFrom(builder, new LimitInputStream(this.inputStream, size),
334             (int)size);
335         } catch (IOException ipbe) {
336           throw (EOFException) new EOFException("Invalid PB, EOF? Ignoring; originalPosition=" +
337             originalPosition + ", currentPosition=" + this.inputStream.getPos() +
338             ", messageSize=" + size + ", currentAvailable=" + available).initCause(ipbe);
339         }
340         if (!builder.isInitialized()) {
341           // TODO: not clear if we should try to recover from corrupt PB that looks semi-legit.
342           //       If we can get the KV count, we could, theoretically, try to get next record.
343           throw new EOFException("Partial PB while reading WAL, " +
344               "probably an unexpected EOF, ignoring");
345         }
346         WALKey walKey = builder.build();
347         entry.getKey().readFieldsFromPb(walKey, this.byteStringUncompressor);
348         if (!walKey.hasFollowingKvCount() || 0 == walKey.getFollowingKvCount()) {
349           LOG.trace("WALKey has no KVs that follow it; trying the next one");
350           continue;
351         }
352         int expectedCells = walKey.getFollowingKvCount();
353         long posBefore = this.inputStream.getPos();
354         try {
355           int actualCells = entry.getEdit().readFromCells(cellDecoder, expectedCells);
356           if (expectedCells != actualCells) {
357             throw new EOFException("Only read " + actualCells); // other info added in catch
358           }
359         } catch (Exception ex) {
360           String posAfterStr = "<unknown>";
361           try {
362             posAfterStr = this.inputStream.getPos() + "";
363           } catch (Throwable t) {
364             LOG.trace("Error getting pos for error message - ignoring", t);
365           }
366           String message = " while reading " + expectedCells + " WAL KVs; started reading at "
367               + posBefore + " and read up to " + posAfterStr;
368           IOException realEofEx = extractHiddenEof(ex);
369           throw (EOFException) new EOFException("EOF " + message).
370               initCause(realEofEx != null ? realEofEx : ex);
371         }
372         if (trailerPresent && this.inputStream.getPos() > this.walEditsStopOffset) {
373           LOG.error("Read WALTrailer while reading WALEdits. wal: " + this.path
374               + ", inputStream.getPos(): " + this.inputStream.getPos() + ", walEditsStopOffset: "
375               + this.walEditsStopOffset);
376           throw new EOFException("Read WALTrailer while reading WALEdits");
377         }
378       } catch (EOFException eof) {
379         LOG.trace("Encountered a malformed edit, seeking back to last good position in file", eof);
380         // If originalPosition is < 0, it is rubbish and we cannot use it (probably local fs)
381         if (originalPosition < 0) throw eof;
382         // Else restore our position to original location in hope that next time through we will
383         // read successfully.
384         seekOnFs(originalPosition);
385         return false;
386       }
387       return true;
388     }
389   }
390 
391   private IOException extractHiddenEof(Exception ex) {
392     // There are two problems we are dealing with here. Hadoop stream throws generic exception
393     // for EOF, not EOFException; and scanner further hides it inside RuntimeException.
394     IOException ioEx = null;
395     if (ex instanceof EOFException) {
396       return (EOFException)ex;
397     } else if (ex instanceof IOException) {
398       ioEx = (IOException)ex;
399     } else if (ex instanceof RuntimeException
400         && ex.getCause() != null && ex.getCause() instanceof IOException) {
401       ioEx = (IOException)ex.getCause();
402     }
403     if (ioEx != null) {
404       if (ioEx.getMessage().contains("EOF")) return ioEx;
405       return null;
406     }
407     return null;
408   }
409 
410   @Override
411   protected void seekOnFs(long pos) throws IOException {
412     this.inputStream.seek(pos);
413   }
414 }