/** * 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.File; import java.io.IOException; import java.util.Collections; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Rolling log implementation. It's a wrapper of SimpleLog. It maintains * a list of log files, once the size of the log file reaches certain threshold, * we'll roll the log. */ class RollingLog implements Log { private static final Logger LOG = LoggerFactory.getLogger(RollingLog.class); /** * The maximum size for each log file. We'll roll the file once current file * reaches the this threshold. (default to 1GB) */ private final long rollingSize; /** * The list of log files, they are sorted by zxid order. */ private final List<File> logFiles = new ArrayList<File>(); /** * The current log. Transaction will be appended to this log file. */ SimpleLog currentLog; /** * The log directory for all the log files. */ private final File logDir; /** * The last seen zxid, used to avoid appending duplicate transactions. */ private Zxid lastSeenZxid = null; /** * Creates a RollingLog object. * * @param logDir the directory contains the rolling log files. * @param rollingSize the size threshold for rolling a new log. * @throws IOException in case of IO failure */ public RollingLog(File logDir, long rollingSize) throws IOException { this.logDir = logDir; this.rollingSize = rollingSize; // Initialize from log directory. initFromDir(); this.currentLog = getLastLog(); this.lastSeenZxid = getLatestZxid(); } /** * Closes the log file and release the resource. * * @throws IOException in case of IO failure */ @Override public void close() throws IOException { if (this.currentLog != null) { this.currentLog.close(); this.currentLog = null; } } /** * 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 (this.lastSeenZxid.compareTo(txn.getZxid()) >= 0) { String exStr = String.format("The zxid %s is not larger than last seen" + " zxid %s in the log.", txn.getZxid(), lastSeenZxid); throw new RuntimeException(exStr); } if (currentLog == null || currentLog.length() >= this.rollingSize) { Zxid zxid = txn.getZxid(); // Close the old one if any. this.close(); File logFile = new File(logDir, "transaction." + zxid.toSimpleString()); LOG.debug("Rolling to the new log {}.", logFile.getName()); // Adds new created log file to list. this.logFiles.add(logFile); this.currentLog = new SimpleLog(logFile); } this.lastSeenZxid = txn.getZxid(); this.currentLog.append(txn); } /** * 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 { int lastKeepIdx = getFileIdx(zxid); for (int i = lastKeepIdx + 1; i < logFiles.size(); ++i) { // Deletes all the log files after the file which contains the // transaction with zxid. File file = logFiles.get(i); boolean result = file.delete(); if (!result) { LOG.warn("The file {} might not be deleted successfully.", file.getName()); } } if (lastKeepIdx != -1) { File file = this.logFiles.get(lastKeepIdx); try (SimpleLog log = new SimpleLog(file)) { log.truncate(zxid); } } logFiles.subList(lastKeepIdx+ 1, logFiles.size()).clear(); this.currentLog = getLastLog(); this.lastSeenZxid = getLatestZxid(); } /** * 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 (logFiles.isEmpty()) { return Zxid.ZXID_NOT_EXIST; } try (SimpleLog log = getLastLog()) { return log.getLatestZxid(); } } /** * 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 { return new RollingLogIterator(zxid); } /** * 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 { int idx = getFileIdx(zxid); if (idx == -1) { Log.LogIterator iter = new RollingLogIterator(Zxid.ZXID_NOT_EXIST); return new DivergingTuple(iter, Zxid.ZXID_NOT_EXIST); } Zxid firstZxid = getZxidFromFileName(logFiles.get(idx)); Log.LogIterator iter = new RollingLogIterator(firstZxid); Zxid prevZxid = firstZxid; while (iter.hasNext()) { Zxid curZxid = iter.next().getZxid(); if (curZxid.compareTo(zxid) == 0) { return new DivergingTuple(iter, zxid); } if (curZxid.compareTo(zxid) > 0) { iter.close(); return new DivergingTuple(new RollingLogIterator(curZxid), 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 { if (this.currentLog != null) { this.currentLog.sync(); } } /** * 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"); } // Initialize from the log directory. void initFromDir() { for (File file : this.logDir.listFiles()) { if (!file.isDirectory() && file.getName().matches("transaction\\.\\d+_\\d+")) { // Appends the file with valid name to log file list. this.logFiles.add(file); } } if (!this.logFiles.isEmpty()) { // Sorts the file by the zxid order. Collections.sort(this.logFiles); } } /** * Given the zxid, find out the idx of the file in list which contains the * transaction with this zxid if and only if the transaction with the zxid * is in RollingLog. If the zxid is smaller than the smallest zxid in log, * -1 will be returned. * * @param zxid the zxid of the transaction. * @return the idx of file which possibly contains the transaction with * given zxid. */ int getFileIdx(Zxid zxid) { if (logFiles.isEmpty() || zxid.compareTo(getZxidFromFileName(logFiles.get(0))) < 0) { // If there's no log files or the zxid is smaller than the smallest zxid // of the rolling log, returns -1. return -1; } int idx = 0; while (idx < logFiles.size() - 1) { Zxid firstZxid = getZxidFromFileName(logFiles.get(idx)); if (zxid.compareTo(firstZxid) == 0) { break; } else if (zxid.compareTo(firstZxid) > 0) { int nextIdx = idx + 1; if (nextIdx < logFiles.size()) { Zxid nextFirstZxid = getZxidFromFileName(logFiles.get(nextIdx)); if (zxid.compareTo(nextFirstZxid) < 0) { // Means the zxid is larger than the smallest allowed zxid of log // file of index i but smaller than the smallest allowed zxid of // log file of index i + 1. So the transaction with zxid only // can be possibly in log file with idx i. break; } } } idx++; } return idx; } /** * Given the log file, finds out the smallest allowed zxid for thi file. It's * infered by looking at the name of the file. * * @return the smallest allowed zxid in this log file. */ Zxid getZxidFromFileName(File file) { String fileName = file.getName(); String strZxid = fileName.substring(fileName.indexOf('.') + 1); return Zxid.fromSimpleString(strZxid); } /** * Gets the last log file in the list of logs. * * @return the SimpleLog instance of the last log. */ SimpleLog getLastLog() throws IOException { if (logFiles.isEmpty()) { return null; } return new SimpleLog(logFiles.get(logFiles.size() - 1)); } /** * An implementation of LogIterator for RollingLog. */ class RollingLogIterator implements Log.LogIterator { int fileIdx; Log.LogIterator iter; public RollingLogIterator(Zxid startZxid) throws IOException { int idx = getFileIdx(startZxid); if (logFiles.isEmpty()) { this.fileIdx = -1; this.iter = null; } else { if (idx == -1) { idx = 0; } this.fileIdx = idx; try (SimpleLog log = new SimpleLog(logFiles.get(this.fileIdx))) { this.iter = log.getIterator(startZxid); } } } /** * Closes the log file and release the resource. * * @throws IOException in case of IO failure */ @Override public void close() throws IOException { if (this.iter != null) { this.iter.close(); } } /** * Checks if it has more transactions. * * @return true if it has more transactions, false otherwise. */ @Override public boolean hasNext() { return this.fileIdx != -1 && (this.iter.hasNext() || this.fileIdx < (logFiles.size() - 1)); } /** * 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(); } if (!this.iter.hasNext()) { this.iter.close(); this.fileIdx++; File nextFile = logFiles.get(this.fileIdx); this.iter = new SimpleLog.SimpleLogIterator(nextFile); } return this.iter.next(); } } }