/*
* Copyright 2013 Jive Software, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.jivesoftware.os.amza.service.storage.binary;
import com.jivesoftware.os.amza.api.filer.IReadable;
import com.jivesoftware.os.amza.api.filer.UIO;
import com.jivesoftware.os.amza.api.scan.RowStream;
import com.jivesoftware.os.amza.api.stream.Fps;
import com.jivesoftware.os.amza.api.stream.RowType;
import com.jivesoftware.os.amza.api.wal.RowIO.PreTruncationNotifier;
import com.jivesoftware.os.amza.api.wal.RowIO.ValidationStream;
import com.jivesoftware.os.amza.api.wal.WALReader;
import com.jivesoftware.os.amza.api.IoStats;
import com.jivesoftware.os.amza.service.storage.filer.WALFiler;
import com.jivesoftware.os.mlogger.core.MetricLogger;
import com.jivesoftware.os.mlogger.core.MetricLoggerFactory;
import java.io.EOFException;
import java.io.IOException;
public class BinaryRowReader implements WALReader {
private static final MetricLogger LOG = MetricLoggerFactory.getLogger();
private final WALFiler parent;
public BinaryRowReader(WALFiler parent) {
this.parent = parent;
}
void validate(IoStats ioStats,
boolean backwardScan,
boolean truncateToLastRowFp,
ValidationStream backward,
ValidationStream forward,
PreTruncationNotifier preTruncationNotifier) throws Exception {
byte[] intLongBuffer = new byte[8];
synchronized (parent.lock()) {
long filerLength = parent.length();
IReadable filer = parent.reader(null, filerLength, true, 0);
if (backwardScan) {
long seekTo = filerLength;
while (seekTo > 0) {
if (seekTo < 4) {
LOG.error("Validation had insufficient bytes to read tail length at offset {} with file length {}", seekTo, filerLength);
break;
}
filer.seek(seekTo - 4);
int tailLength = UIO.readInt(filer, "length", intLongBuffer);
if (tailLength <= 0 || tailLength >= filerLength) {
LOG.error("Validation found tail length of {} at offset {} with file length {}", tailLength, seekTo, filerLength);
break;
}
seekTo = seekTo - tailLength - 8;
if (seekTo < 0) {
LOG.error("Validation required seek to {} with file length {}", seekTo, filerLength);
break;
} else {
filer.seek(seekTo);
int headLength = UIO.readInt(filer, "length", intLongBuffer);
if (tailLength != headLength) {
LOG.warn("Validation read a head length of {} but a tail length of {} at offset {} with file length {}",
headLength, tailLength, seekTo, filerLength);
break;
}
RowType rowType = RowType.fromByte((byte) filer.read());
long rowTxId = UIO.readLong(filer, "txId", intLongBuffer);
byte[] row = new byte[headLength - (1 + 8)];
filer.read(row);
long truncateAfterRowAtFp;
try {
truncateAfterRowAtFp = backward.row(seekTo, rowTxId, rowType, row);
} catch (IOException e) {
LOG.warn("Validation encountered an I/O exception at offset {} with file length {}", new Object[] { seekTo, filerLength }, e);
break;
}
if (truncateAfterRowAtFp > -1) {
if (truncateToLastRowFp) {
filer.seek(truncateAfterRowAtFp);
headLength = UIO.readInt(filer, "length", intLongBuffer);
long truncatedLength = truncateAfterRowAtFp + 4 + headLength + 4;
if (truncatedLength != filerLength) {
LOG.warn("Truncating after row at fp {} for reverse scan", truncateAfterRowAtFp);
truncate(preTruncationNotifier, truncatedLength);
}
}
return;
}
}
if (seekTo == 0) {
if (truncateToLastRowFp) {
LOG.warn("Truncating entire WAL");
truncate(preTruncationNotifier, 0);
}
return;
}
}
}
long[] truncateAfterRowAtFp = new long[] { Long.MIN_VALUE };
scan(ioStats, 0, true, true, 1024 * 1024, preTruncationNotifier, (rowFP, rowTxId, rowType, row) -> {
long result = forward.row(rowFP, rowTxId, rowType, row);
if (result != -1) {
if (result < -1) {
truncateAfterRowAtFp[0] = -(result + 1);
}
if (result > -1) {
return false;
}
}
return true;
});
if (truncateAfterRowAtFp[0] == Long.MIN_VALUE) {
if (truncateToLastRowFp) {
LOG.warn("Truncating entire WAL due to missing truncation feedback");
truncate(preTruncationNotifier, 0);
}
} else {
// Have to reacquire filer because scan may have truncated
filer = parent.reader(filer, parent.length(), false, 0);
filer.seek(truncateAfterRowAtFp[0]);
long headLength = UIO.readInt(filer, "length", intLongBuffer);
long truncatedLength = truncateAfterRowAtFp[0] + 4 + headLength + 4;
if (truncatedLength < filer.length()) {
LOG.warn("Truncating after row at fp {} for forward scan", truncateAfterRowAtFp[0]);
truncate(preTruncationNotifier, truncatedLength);
}
}
}
}
@Override
public boolean reverseScan(IoStats ioStats, RowStream stream) throws Exception {
return reverseScan(ioStats, stream, false, 0); //TODO config
}
private boolean reverseScan(IoStats ioStats, RowStream stream, boolean fallBackToChannelReader, int bufferSize) throws Exception {
long boundaryFp = parent.length(); // last length int
IReadable parentFiler = parent.reader(null, boundaryFp, fallBackToChannelReader, bufferSize);
if (boundaryFp < 0) {
return true;
}
long read = 0;
try {
byte[] intLongBuffer = new byte[8];
long seekTo = boundaryFp - 4;
while (true) {
long rowFP;
RowType rowType;
long rowTxId;
byte[] row;
if (seekTo >= 0) {
parentFiler.seek(seekTo);
int priorLength = UIO.readInt(parentFiler, "priorLength", intLongBuffer);
seekTo -= (priorLength + 4);
if (seekTo < 0) {
break;
}
parentFiler.seek(seekTo);
int length = UIO.readInt(parentFiler, "length", intLongBuffer);
rowType = RowType.fromByte((byte) parentFiler.read());
rowTxId = UIO.readLong(parentFiler, "txId", intLongBuffer);
row = new byte[length - (1 + 8)];
parentFiler.read(row);
rowFP = seekTo;
read += (parentFiler.getFilePointer() - seekTo);
seekTo -= 4;
} else {
break;
}
if (!stream.row(rowFP, rowTxId, rowType, row)) {
return false;
}
}
return true;
} finally {
ioStats.read.add(read);
}
}
@Override
public boolean scan(IoStats ioStats, long offsetFp, boolean allowRepairs, RowStream stream) throws Exception {
return scan(ioStats, offsetFp, allowRepairs, false, 1024 * 1024, null, stream); //TODO config
}
private boolean scan(IoStats ioStats,
long offsetFp,
boolean allowRepairs,
boolean fallBackToChannelReader,
int bufferSize,
PreTruncationNotifier preTruncationNotifier,
RowStream stream) throws Exception {
long fileLength = 0;
long read = 0;
try {
IReadable filer = null;
byte[] rowTypeByte = new byte[1];
byte[] intLongBuffer = new byte[8];
while (fileLength < parent.length()) {
fileLength = parent.length();
filer = parent.reader(filer, fileLength, fallBackToChannelReader, bufferSize);
while (true) {
long rowFP;
long rowTxId = -1;
RowType rowType = null;
byte[] row = null;
filer.seek(offsetFp);
if (offsetFp < fileLength) {
rowFP = offsetFp;
int length = -1;
try {
length = UIO.readInt(filer, "length", intLongBuffer);
} catch (IOException x) {
if (!allowRepairs) {
throw x;
}
}
int lengthOfTypeAndTxId = 1 + 8;
if (length < lengthOfTypeAndTxId || offsetFp + length + 8 > fileLength) {
if (allowRepairs) {
LOG.warn("Truncating due to corruption while scanning");
return truncate(preTruncationNotifier, offsetFp);
} else {
String msg = "Scan terminated prematurely due to a corruption at fp:" + offsetFp +
" length:" + length +
" available:" + fileLength +
" in " + parent;
LOG.error(msg);
throw new EOFException(msg);
}
}
int trailingLength = -1;
try {
filer.read(rowTypeByte);
rowType = RowType.fromByte(rowTypeByte[0]);
rowTxId = UIO.readLong(filer, "txId", intLongBuffer);
row = new byte[length - lengthOfTypeAndTxId];
if (row.length > 0) {
filer.read(row);
}
trailingLength = UIO.readInt(filer, "length", intLongBuffer);
} catch (IOException x) {
if (!allowRepairs) {
throw x;
}
}
if (trailingLength < 0 || trailingLength != length) {
if (allowRepairs) {
LOG.warn("Truncating due to head-tail length mismatch while scanning");
return truncate(preTruncationNotifier, offsetFp);
} else {
throw new IOException("The lead length of " + length + " didn't equal trailing length of " + trailingLength);
}
}
long fp = filer.getFilePointer();
read += (fp - offsetFp);
offsetFp = fp;
} else {
break;
}
if (rowType != null) {
try {
if (!stream.row(rowFP, rowTxId, rowType, row)) {
return false;
}
} catch (IOException e) {
if (allowRepairs) {
LOG.warn("Encountered I/O exception while streaming rows, we need to truncate", e);
return truncate(preTruncationNotifier, rowFP);
} else {
throw e;
}
}
}
}
}
return true;
} finally {
ioStats.read.add(read);
}
}
private boolean truncate(PreTruncationNotifier preTruncationNotifier, long offsetFp) throws Exception {
if (preTruncationNotifier != null) {
preTruncationNotifier.truncated(offsetFp);
}
synchronized (parent.lock()) {
long before = parent.length();
parent.truncate(offsetFp);
LOG.warn("Truncated WAL at {}, before={} after={} {}", offsetFp, before, parent.length(), parent);
return false;
}
}
@Override
public byte[] readTypeByteTxIdAndRow(long position) throws IOException {
int length = -1;
try {
IReadable filer = parent.reader(null, position + 4, false, 0);
filer.seek(position);
length = UIO.readInt(filer, "length", new byte[4]);
filer = parent.reader(filer, position + 4 + length, false, 0);
filer.seek(position + 4);
byte[] row = new byte[length];
if (row.length > 0) {
filer.read(row);
}
return row;
} catch (NegativeArraySizeException x) {
LOG.error("FAILED to read length:" + length + " bytes at position:" + position + " in file:" + parent);
throw x;
}
}
@Override
public boolean read(IoStats ioStats, Fps fps, RowStream rowStream) throws Exception {
IReadable[] filerRef = { parent.reader(null, 0, false, 0) };
byte[] rawLength = new byte[4];
byte[] rowTypeByteAndTxId = new byte[1 + 8];
return fps.consume(fp -> {
int length = -1;
try {
IReadable filer = parent.reader(filerRef[0], fp + 4, false, 0);
filer.seek(fp);
filer.read(rawLength);
length = UIO.bytesInt(rawLength);
filer = parent.reader(filer, fp + 4 + length, false, 0);
filer.seek(fp + 4);
filer.read(rowTypeByteAndTxId);
RowType rowType = RowType.fromByte(rowTypeByteAndTxId[0]);
long rowTxId = UIO.bytesLong(rowTypeByteAndTxId, 1);
byte[] row = new byte[length - (1 + 8)];
if (row.length > 0) {
filer.read(row);
}
filerRef[0] = filer;
ioStats.read.add(4 + length);
return rowStream.row(fp, rowTxId, rowType, row);
} catch (NegativeArraySizeException x) {
LOG.error("FAILED to read length:" + length + " bytes at position:" + fp + " in file:" + parent);
throw x;
}
});
}
public void hackTruncation(int numBytes) {
try {
truncate(null, parent.length() - numBytes);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public long length() throws IOException {
return parent.length();
}
void close() throws IOException {
parent.close();
}
}