/** * 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; } } }