1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 import com.google.protobuf.InvalidProtocolBufferException;
49
50
51
52
53
54
55
56
57
58
59
60
61 @InterfaceAudience.LimitedPrivate({HBaseInterfaceAudience.COPROC, HBaseInterfaceAudience.PHOENIX,
62 HBaseInterfaceAudience.CONFIG})
63 public class ProtobufLogReader extends ReaderBase {
64 private static final Log LOG = LogFactory.getLog(ProtobufLogReader.class);
65
66 @InterfaceAudience.Private
67 public static final byte[] PB_WAL_MAGIC = Bytes.toBytes("PWAL");
68
69 @InterfaceAudience.Private
70 public static final byte[] PB_WAL_COMPLETE_MAGIC = Bytes.toBytes("LAWP");
71
72
73
74
75 static final String WAL_TRAILER_WARN_SIZE = "hbase.regionserver.waltrailer.warn.size";
76 static final int DEFAULT_WAL_TRAILER_WARN_SIZE = 1024 * 1024;
77
78 protected FSDataInputStream inputStream;
79 protected Codec.Decoder cellDecoder;
80 protected WALCellCodec.ByteStringUncompressor byteStringUncompressor;
81 protected boolean hasCompression = false;
82 protected boolean hasTagCompression = false;
83
84
85 private long walEditsStopOffset;
86 private boolean trailerPresent;
87 protected WALTrailer trailer;
88
89
90 protected int trailerWarnSize;
91 private static List<String> writerClsNames = new ArrayList<String>();
92 static {
93 writerClsNames.add(ProtobufLogWriter.class.getSimpleName());
94 }
95
96
97 private String codecClsName = null;
98
99 @InterfaceAudience.Private
100 public long trailerSize() {
101 if (trailerPresent) {
102
103 final long calculatedSize = (long) PB_WAL_COMPLETE_MAGIC.length + Bytes.SIZEOF_INT +
104 trailer.getSerializedSize();
105 final long expectedSize = fileLength - walEditsStopOffset;
106 if (expectedSize != calculatedSize) {
107 LOG.warn("After parsing the trailer, we expect the total footer to be "+ expectedSize
108 + " bytes, but we calculate it as being " + calculatedSize);
109 }
110 return expectedSize;
111 } else {
112 return -1L;
113 }
114 }
115
116 enum WALHdrResult {
117 EOF,
118 SUCCESS,
119 UNKNOWN_WRITER_CLS
120 }
121
122
123 static class WALHdrContext {
124 WALHdrResult result;
125 String cellCodecClsName;
126
127 WALHdrContext(WALHdrResult result, String cellCodecClsName) {
128 this.result = result;
129 this.cellCodecClsName = cellCodecClsName;
130 }
131 WALHdrResult getResult() {
132 return result;
133 }
134 String getCellCodecClsName() {
135 return cellCodecClsName;
136 }
137 }
138
139 public ProtobufLogReader() {
140 super();
141 }
142
143 @Override
144 public void close() throws IOException {
145 if (this.inputStream != null) {
146 this.inputStream.close();
147 this.inputStream = null;
148 }
149 }
150
151 @Override
152 public long getPosition() throws IOException {
153 return inputStream.getPos();
154 }
155
156 @Override
157 public void reset() throws IOException {
158 String clsName = initInternal(null, false);
159 initAfterCompression(clsName);
160 }
161
162 @Override
163 public void init(FileSystem fs, Path path, Configuration conf, FSDataInputStream stream)
164 throws IOException {
165 this.trailerWarnSize = conf.getInt(WAL_TRAILER_WARN_SIZE, DEFAULT_WAL_TRAILER_WARN_SIZE);
166 super.init(fs, path, conf, stream);
167 }
168
169 @Override
170 protected String initReader(FSDataInputStream stream) throws IOException {
171 return initInternal(stream, true);
172 }
173
174
175
176
177 public List<String> getWriterClsNames() {
178 return writerClsNames;
179 }
180
181
182
183
184 public String getCodecClsName() {
185 return codecClsName;
186 }
187
188 protected WALHdrContext readHeader(Builder builder, FSDataInputStream stream)
189 throws IOException {
190 boolean res = builder.mergeDelimitedFrom(stream);
191 if (!res) return new WALHdrContext(WALHdrResult.EOF, null);
192 if (builder.hasWriterClsName() &&
193 !getWriterClsNames().contains(builder.getWriterClsName())) {
194 return new WALHdrContext(WALHdrResult.UNKNOWN_WRITER_CLS, null);
195 }
196 String clsName = null;
197 if (builder.hasCellCodecClsName()) {
198 clsName = builder.getCellCodecClsName();
199 }
200 return new WALHdrContext(WALHdrResult.SUCCESS, clsName);
201 }
202
203 private String initInternal(FSDataInputStream stream, boolean isFirst)
204 throws IOException {
205 close();
206 long expectedPos = PB_WAL_MAGIC.length;
207 if (stream == null) {
208 stream = fs.open(path);
209 stream.seek(expectedPos);
210 }
211 if (stream.getPos() != expectedPos) {
212 throw new IOException("The stream is at invalid position: " + stream.getPos());
213 }
214
215 WALProtos.WALHeader.Builder builder = WALProtos.WALHeader.newBuilder();
216 WALHdrContext hdrCtxt = readHeader(builder, stream);
217 WALHdrResult walHdrRes = hdrCtxt.getResult();
218 if (walHdrRes == WALHdrResult.EOF) {
219 throw new EOFException("Couldn't read WAL PB header");
220 }
221 if (walHdrRes == WALHdrResult.UNKNOWN_WRITER_CLS) {
222 throw new IOException("Got unknown writer class: " + builder.getWriterClsName());
223 }
224 if (isFirst) {
225 WALProtos.WALHeader header = builder.build();
226 this.hasCompression = header.hasHasCompression() && header.getHasCompression();
227 this.hasTagCompression = header.hasHasTagCompression() && header.getHasTagCompression();
228 }
229 this.inputStream = stream;
230 this.walEditsStopOffset = this.fileLength;
231 long currentPosition = stream.getPos();
232 trailerPresent = setTrailerIfPresent();
233 this.seekOnFs(currentPosition);
234 if (LOG.isTraceEnabled()) {
235 LOG.trace("After reading the trailer: walEditsStopOffset: " + this.walEditsStopOffset
236 + ", fileLength: " + this.fileLength + ", " + "trailerPresent: " + (trailerPresent ? "true, size: " + trailer.getSerializedSize() : "false") + ", currentPosition: " + currentPosition);
237 }
238
239 codecClsName = hdrCtxt.getCellCodecClsName();
240
241 return hdrCtxt.getCellCodecClsName();
242 }
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261 private boolean setTrailerIfPresent() {
262 try {
263 long trailerSizeOffset = this.fileLength - (PB_WAL_COMPLETE_MAGIC.length + Bytes.SIZEOF_INT);
264 if (trailerSizeOffset <= 0) return false;
265 this.seekOnFs(trailerSizeOffset);
266
267 int trailerSize = this.inputStream.readInt();
268 ByteBuffer buf = ByteBuffer.allocate(ProtobufLogReader.PB_WAL_COMPLETE_MAGIC.length);
269 this.inputStream.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
270 if (!Arrays.equals(buf.array(), PB_WAL_COMPLETE_MAGIC)) {
271 LOG.trace("No trailer found.");
272 return false;
273 }
274 if (trailerSize < 0) {
275 LOG.warn("Invalid trailer Size " + trailerSize + ", ignoring the trailer");
276 return false;
277 } else if (trailerSize > this.trailerWarnSize) {
278
279 LOG.warn("Please investigate WALTrailer usage. Trailer size > maximum configured size : "
280 + trailerSize + " > " + this.trailerWarnSize);
281 }
282
283 long positionOfTrailer = trailerSizeOffset - trailerSize;
284 this.seekOnFs(positionOfTrailer);
285
286 buf = ByteBuffer.allocate(trailerSize);
287 this.inputStream.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
288 trailer = WALTrailer.parseFrom(buf.array());
289 this.walEditsStopOffset = positionOfTrailer;
290 return true;
291 } catch (IOException ioe) {
292 LOG.warn("Got IOE while reading the trailer. Continuing as if no trailer is present.", ioe);
293 }
294 return false;
295 }
296
297 protected WALCellCodec getCodec(Configuration conf, String cellCodecClsName,
298 CompressionContext compressionContext) throws IOException {
299 return WALCellCodec.create(conf, cellCodecClsName, compressionContext);
300 }
301
302 @Override
303 protected void initAfterCompression() throws IOException {
304 initAfterCompression(null);
305 }
306
307 @Override
308 protected void initAfterCompression(String cellCodecClsName) throws IOException {
309 WALCellCodec codec = getCodec(this.conf, cellCodecClsName, this.compressionContext);
310 this.cellDecoder = codec.getDecoder(this.inputStream);
311 if (this.hasCompression) {
312 this.byteStringUncompressor = codec.getByteStringUncompressor();
313 }
314 }
315
316 @Override
317 protected boolean hasCompression() {
318 return this.hasCompression;
319 }
320
321 @Override
322 protected boolean hasTagCompression() {
323 return this.hasTagCompression;
324 }
325
326 @Override
327 protected boolean readNext(Entry entry) throws IOException {
328 while (true) {
329
330 long originalPosition = this.inputStream.getPos();
331 if (trailerPresent && originalPosition > 0 && originalPosition == this.walEditsStopOffset) {
332 if (LOG.isTraceEnabled()) {
333 LOG.trace("Reached end of expected edits area at offset " + originalPosition);
334 }
335 return false;
336 }
337 WALKey.Builder builder = WALKey.newBuilder();
338 long size = 0;
339 try {
340 long available = -1;
341 try {
342 int firstByte = this.inputStream.read();
343 if (firstByte == -1) {
344 throw new EOFException("First byte is negative at offset " + originalPosition);
345 }
346 size = CodedInputStream.readRawVarint32(firstByte, this.inputStream);
347
348 available = this.inputStream.available();
349 if (available > 0 && available < size) {
350 throw new EOFException("Available stream not enough for edit, " +
351 "inputStream.available()= " + this.inputStream.available() + ", " +
352 "entry size= " + size + " at offset = " + this.inputStream.getPos());
353 }
354 ProtobufUtil.mergeFrom(builder, new LimitInputStream(this.inputStream, size),
355 (int)size);
356 } catch (InvalidProtocolBufferException ipbe) {
357 throw (EOFException) new EOFException("Invalid PB, EOF? Ignoring; originalPosition=" +
358 originalPosition + ", currentPosition=" + this.inputStream.getPos() +
359 ", messageSize=" + size + ", currentAvailable=" + available).initCause(ipbe);
360 }
361 if (!builder.isInitialized()) {
362
363
364 throw new EOFException("Partial PB while reading WAL, " +
365 "probably an unexpected EOF, ignoring. current offset=" + this.inputStream.getPos());
366 }
367 WALKey walKey = builder.build();
368 entry.getKey().readFieldsFromPb(walKey, this.byteStringUncompressor);
369 if (!walKey.hasFollowingKvCount() || 0 == walKey.getFollowingKvCount()) {
370 if (LOG.isTraceEnabled()) {
371 LOG.trace("WALKey has no KVs that follow it; trying the next one. current offset=" + this.inputStream.getPos());
372 }
373 continue;
374 }
375 int expectedCells = walKey.getFollowingKvCount();
376 long posBefore = this.inputStream.getPos();
377 try {
378 int actualCells = entry.getEdit().readFromCells(cellDecoder, expectedCells);
379 if (expectedCells != actualCells) {
380 throw new EOFException("Only read " + actualCells);
381 }
382 } catch (Exception ex) {
383 String posAfterStr = "<unknown>";
384 try {
385 posAfterStr = this.inputStream.getPos() + "";
386 } catch (Throwable t) {
387 if (LOG.isTraceEnabled()) {
388 LOG.trace("Error getting pos for error message - ignoring", t);
389 }
390 }
391 String message = " while reading " + expectedCells + " WAL KVs; started reading at "
392 + posBefore + " and read up to " + posAfterStr;
393 IOException realEofEx = extractHiddenEof(ex);
394 throw (EOFException) new EOFException("EOF " + message).
395 initCause(realEofEx != null ? realEofEx : ex);
396 }
397 if (trailerPresent && this.inputStream.getPos() > this.walEditsStopOffset) {
398 LOG.error("Read WALTrailer while reading WALEdits. wal: " + this.path
399 + ", inputStream.getPos(): " + this.inputStream.getPos() + ", walEditsStopOffset: "
400 + this.walEditsStopOffset);
401 throw new EOFException("Read WALTrailer while reading WALEdits");
402 }
403 } catch (EOFException eof) {
404
405 if (originalPosition < 0) {
406 if (LOG.isTraceEnabled()) {
407 LOG.trace("Encountered a malformed edit, but can't seek back to last good position because originalPosition is negative. last offset=" + this.inputStream.getPos(), eof);
408 }
409 throw eof;
410 }
411
412
413 if (LOG.isTraceEnabled()) {
414 LOG.trace("Encountered a malformed edit, seeking back to last good position in file, from "+ inputStream.getPos()+" to " + originalPosition, eof);
415 }
416 seekOnFs(originalPosition);
417 return false;
418 }
419 return true;
420 }
421 }
422
423 private IOException extractHiddenEof(Exception ex) {
424
425
426 IOException ioEx = null;
427 if (ex instanceof EOFException) {
428 return (EOFException)ex;
429 } else if (ex instanceof IOException) {
430 ioEx = (IOException)ex;
431 } else if (ex instanceof RuntimeException
432 && ex.getCause() != null && ex.getCause() instanceof IOException) {
433 ioEx = (IOException)ex.getCause();
434 }
435 if (ioEx != null) {
436 if (ioEx.getMessage().contains("EOF")) return ioEx;
437 return null;
438 }
439 return null;
440 }
441
442 @Override
443 protected void seekOnFs(long pos) throws IOException {
444 this.inputStream.seek(pos);
445 }
446 }