/**
* Licensed to the zk1931 under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* The ASF licenses this file to you 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.github.zk1931.jzab;
import com.github.zk1931.jzab.Log.DivergingTuple;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.NoSuchElementException;
import java.util.zip.Adler32;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class implements the Log interface.
* The format of a Transaction log is as follows:
*
* <p>
* <pre>
* log-file := [ transactions ]
*
* transactions := transaction | transaction transactions
*
* transaction := checksum length zxid type payload
*
* checksum := checksum(int)
*
* length := length of zxid + type + payload
*
* zxid := epoch(long) xid(long)
*
* type := type(int)
*
* payload := byte array
* </pre>
*/
class SimpleLog implements Log {
private static final Logger LOG = LoggerFactory.getLogger(SimpleLog.class);
private final File logFile;
private final DataOutputStream logStream;
private final FileOutputStream fout;
private Zxid lastSeenZxid = null;
// The number of bytes for Zxid field of transaction.
private static final int ZXID_LENGTH = 16;
// The number of bytes for type field of transaction.
private static final int TYPE_LENGTH = 4;
// The number of bytes for checksum field of transaction.
private static final int CHECKSUM_LENGTH = 4;
// The number of bytes for length field of transaction.
private static final int LENGTH_LENGTH = 4;
/**
* Creates a transaction log. The logFile can be either
* a new file or an existing file. If it's an existing
* file, then the new log will be appended to the end of
* the log.
*
* @param logFile the log file
* @throws IOException in case of IO failure
*/
public SimpleLog(File logFile) throws IOException {
this.logFile = logFile;
this.fout = new FileOutputStream(logFile, true);
this.logStream = new DataOutputStream(
new BufferedOutputStream(fout));
this.lastSeenZxid = getLatestZxid();
LOG.debug("SimpleLog constructed. The lastSeenZxid is {}.",
this.lastSeenZxid);
}
/**
* Closes the log file and release the resource.
*
* @throws IOException in case of IO failure
*/
@Override
public void close() throws IOException {
this.logStream.close();
}
/**
* Appends a request to transaction log.
*
* @param txn the transaction which will be added to log.
* @throws IOException in case of IO failure
*/
@Override
public void append(Transaction txn) throws IOException {
if(txn.getZxid().compareTo(this.lastSeenZxid) <= 0) {
LOG.error("Cannot append {}. lastSeenZxid = {}",
txn.getZxid(), this.lastSeenZxid);
throw new RuntimeException("The id of the transaction is less "
+ "than the id of last seen transaction");
}
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
ByteBuffer payload = txn.getBody();
// The number of bytes for Zxid + Type + payload.
int length = payload.remaining() + ZXID_LENGTH + TYPE_LENGTH;
// Writes the length.
dout.writeInt(length);
// Writes Zxid.
dout.writeLong(txn.getZxid().getEpoch());
dout.writeLong(txn.getZxid().getXid());
// Writes the type of the transaction.
dout.writeInt(txn.getType());
// Writes the body of the transaction.
while (payload.hasRemaining()) {
dout.writeByte(payload.get());
}
dout.flush();
byte[] blob = bout.toByteArray();
// Calculates the checksum.
Adler32 checksum = new Adler32();
checksum.update(blob);
// Gets the checksum value.
int checksumValue = (int)checksum.getValue();
this.logStream.writeInt(checksumValue);
this.logStream.write(blob);
this.logStream.flush();
// Update last seen Zxid.
this.lastSeenZxid = txn.getZxid();
} catch(IOException e) {
this.logStream.close();
}
}
/**
* Truncates this transaction log at the given zxid.
* This method deletes all the transactions with zxids
* higher than the given zxid.
*
* @param zxid the transaction id.
* @throws IOException in case of IO failure
*/
@Override
public void truncate(Zxid zxid) throws IOException {
try (SimpleLogIterator iter = new SimpleLogIterator(this.logFile)) {
this.lastSeenZxid = Zxid.ZXID_NOT_EXIST;
while (iter.hasNext()) {
Transaction txn = iter.next();
if (txn.getZxid().compareTo(zxid) == 0) {
this.lastSeenZxid = txn.getZxid();
break;
}
if (txn.getZxid().compareTo(zxid) > 0) {
iter.backward();
break;
}
this.lastSeenZxid = txn.getZxid();
}
if (iter.hasNext()) {
// It means there's something to truncate.
try (RandomAccessFile ra = new RandomAccessFile(this.logFile, "rw")) {
// Truncate the file from given position.
ra.setLength(iter.getPosition());
}
}
}
}
/**
* Gets the latest appended transaction id from the log.
*
* @return the transaction id of the latest transaction.
* or Zxid.ZXID_NOT_EXIST if the log is empty.
* @throws IOException in case of IO failure
*/
@Override
public Zxid getLatestZxid() throws IOException {
if (this.lastSeenZxid != null) {
// If the lastSeenZxid is cached, returns it directly.
return this.lastSeenZxid;
}
Transaction txn = null;
try (LogIterator iter = new SimpleLogIterator(this.logFile)) {
while (iter.hasNext()) {
txn = iter.next();
}
if (txn == null) {
return Zxid.ZXID_NOT_EXIST;
}
return txn.getZxid();
}
}
/**
* Gets an iterator to read transactions from this log starting
* at the given zxid (including zxid).
*
* @param zxid the id of the transaction.
* @return an iterator to read the next transaction in logs.
* @throws IOException in case of IO failure
*/
@Override
public LogIterator getIterator(Zxid zxid) throws IOException {
SimpleLogIterator iter = new SimpleLogIterator(this.logFile);
while(iter.hasNext()) {
Transaction txn = iter.next();
if(txn.getZxid().compareTo(zxid) >= 0) {
iter.backward();
break;
}
}
return iter;
}
/**
* See {@link Log#firstDivergingPoint}.
*
* @param zxid the id of the transaction.
* @return a tuple holds first diverging zxid and an iterator points to
* subsequent transactions.
* @throws IOException in case of IO failures
*/
@Override
public DivergingTuple firstDivergingPoint(Zxid zxid) throws IOException {
SimpleLogIterator iter =
(SimpleLogIterator)getIterator(Zxid.ZXID_NOT_EXIST);
Zxid prevZxid = Zxid.ZXID_NOT_EXIST;
while (iter.hasNext()) {
Zxid curZxid = iter.next().getZxid();
if (curZxid.compareTo(zxid) == 0) {
return new DivergingTuple(iter, zxid);
}
if (curZxid.compareTo(zxid) > 0) {
iter.backward();
return new DivergingTuple(iter, prevZxid);
}
prevZxid = curZxid;
}
return new DivergingTuple(iter, prevZxid);
}
/**
* Syncs all the appended transactions to the physical media.
*
* @throws IOException in case of IO failure
*/
@Override
public void sync() throws IOException {
this.logStream.flush();
this.fout.getChannel().force(false);
}
/**
* Trim the log up to the transaction with Zxid zxid inclusively.
*
* @param zxid the last zxid(inclusive) which will be trimed to.
* @throws IOException in case of IO failures
*/
@Override
public void trim(Zxid zxid) throws IOException {
throw new UnsupportedOperationException("Not supported");
}
long length() {
return this.logFile.length();
}
String getName() {
return this.logFile.getName();
}
/**
* An implementation of iterator for iterating the log.
*/
public static class SimpleLogIterator implements Log.LogIterator {
private DataInputStream logStream;
private final FileInputStream fin;
private final File logFile;
private int position = 0;
private int lastTransactionLength = 0;
private Zxid prevZxid = Zxid.ZXID_NOT_EXIST;
public SimpleLogIterator(File logFile) throws IOException {
this.logFile = logFile;
this.fin = new FileInputStream(logFile);
this.logStream = new DataInputStream(new BufferedInputStream(this.fin));
}
/**
* Gets the position of this iterator in file.
* @return the position in file
*/
public int getPosition() {
return this.position;
}
/**
* Closes the log file and release the resource.
*
* @throws IOException in case of IO failure
*/
@Override
public void close() throws IOException {
this.logStream.close();
}
/**
* Checks if it has more transactions.
*
* @return true if it has more transactions, false otherwise.
*/
@Override
public boolean hasNext() {
return this.position < this.logFile.length();
}
/**
* Goes to the next transaction record.
*
* @return the next transaction record
* @throws java.io.EOFException if it reaches the end of file before reading
* the entire transaction.
* @throws IOException in case of IO failure
* @throws NoSuchElementException
* if there's no more elements to get
*/
@Override
public Transaction next() throws IOException {
if(!hasNext()) {
throw new NoSuchElementException();
}
DataInputStream in = new DataInputStream(logStream);
if (in.available() < CHECKSUM_LENGTH + LENGTH_LENGTH) {
LOG.error("Not enough bytes for checksum field and length field.");
throw new RuntimeException("Corrupted file.");
}
// Gets the checksum value.
int checksumValue = in.readInt();
int length = in.readInt();
long epoch, xid;
int type;
if (length < ZXID_LENGTH + TYPE_LENGTH) {
LOG.error("The length field is invalid. Previous txn is {}", prevZxid);
throw new RuntimeException("The length field is invalid.");
}
byte[] rest = new byte[length];
in.readFully(rest, 0, length);
byte[] blob = ByteBuffer.allocate(length + LENGTH_LENGTH).putInt(length)
.put(rest)
.array();
// Caculates the checksum.
Adler32 checksum = new Adler32();
checksum.update(blob);
if ((int)checksum.getValue() != checksumValue) {
String exStr =
String.format("Checksum after txn %s mismathes in file %s, file "
+ "corrupted?",
prevZxid, logFile.getName());
LOG.error(exStr);
throw new RuntimeException(exStr);
}
// Checksum is correct, parse the byte array.
ByteArrayInputStream bin = new ByteArrayInputStream(rest);
DataInputStream din = new DataInputStream(bin);
// Reads the Zxid.
epoch = din.readLong();
xid = din.readLong();
// Reads the type of transaction.
type = din.readInt();
int payloadLength = length - ZXID_LENGTH - TYPE_LENGTH;
byte[] payload = new byte[payloadLength];
// Reads the data of the transaction body.
din.readFully(payload, 0, payloadLength);
din.close();
Zxid zxid = new Zxid(epoch, xid);
this.prevZxid = zxid;
this.lastTransactionLength = CHECKSUM_LENGTH + LENGTH_LENGTH + length;
// Updates the position of file.
this.position += this.lastTransactionLength;
return new Transaction(zxid, type, ByteBuffer.wrap(payload));
}
// Moves the transaction log backward to last transaction.
void backward() throws IOException {
this.position -= this.lastTransactionLength;
this.fin.getChannel().position(this.position);
// Since we moved the file pointer, the buffered data should be
// invalidated.
this.logStream = new DataInputStream(new BufferedInputStream(this.fin));
this.lastTransactionLength = 0;
}
}
}