| /*- |
| * Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved. |
| * |
| * This file was distributed by Oracle as part of a version of Oracle Berkeley |
| * DB Java Edition made available at: |
| * |
| * http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html |
| * |
| * Please see the LICENSE file included in the top-level directory of the |
| * appropriate version of Oracle Berkeley DB Java Edition for a copy of the |
| * license and additional information. |
| */ |
| |
| package com.sleepycat.je.log; |
| |
| import java.io.FileNotFoundException; |
| import java.nio.ByteBuffer; |
| import java.util.logging.Logger; |
| |
| import com.sleepycat.je.DatabaseException; |
| import com.sleepycat.je.EnvironmentFailureException; |
| import com.sleepycat.je.config.EnvironmentParams; |
| import com.sleepycat.je.dbi.DbConfigManager; |
| import com.sleepycat.je.dbi.EnvironmentFailureReason; |
| import com.sleepycat.je.dbi.EnvironmentImpl; |
| import com.sleepycat.je.utilint.DbLsn; |
| import com.sleepycat.je.utilint.LoggerUtils; |
| import com.sleepycat.je.utilint.VLSN; |
| |
| /** |
| * A FileReader is an abstract class that traverses the log files, reading-in |
| * chunks of the file at a time. The class provides a iterator interface, via |
| * its readNextEntry() method. Concrete subclasses must (a) provide the public |
| * methods that allow their users to examine the contents of the logrec that |
| * the iterator is currently positioned at, and (b) implement the (non-public) |
| * isTarget() and processEntry() methods that may filter-out logrecs that do |
| * not need to be seen by the caller of readNextEntry(), or perform specific |
| * actions before each logrec is "returned" to the caller of readNextEntry(). |
| */ |
| public abstract class FileReader { |
| |
| protected final EnvironmentImpl envImpl; |
| |
| protected final FileManager fileManager; |
| |
| /* |
| * The ReadWindow is a data buffer that acts as a sliding window view |
| * of the log. It is positioned against the log and filled up with data. |
| */ |
| protected final ReadWindow window; |
| |
| /* |
| * For piecing together a log entry that is read from multiple read buffer |
| * calls. |
| */ |
| private ByteBuffer saveBuffer; |
| |
| private final boolean singleFile;// if true, do not read across files |
| |
| /* |
| * true if at end of the log. |
| * TODO: assess whether this is redundant with the EOFException, and |
| * could be streamlined. |
| */ |
| protected boolean eof; |
| |
| /* if true, we're reading forward; otherwise backwards */ |
| protected final boolean forward; |
| |
| /* num entries we've seen */ |
| private int nRead; |
| |
| /* The log entry header for the entry that was just read. */ |
| protected LogEntryHeader currentEntryHeader; |
| |
| /* |
| * The log entry before the current entry. In general, |
| * currentEntryPrevOffset is the same as |
| * currentEntryHeader.getPrevOffset(), but it's initialized and used before |
| * a header is read. Only used for backward scanning. |
| */ |
| protected long currentEntryPrevOffset; |
| |
| /* |
| * nextEntryOffset is used to set the currentEntryOffset after we've read |
| * an entry. Only used for forward scanning. |
| */ |
| protected long currentEntryOffset; |
| protected long nextEntryOffset; |
| |
| protected long startLsn; // We start reading from this LSN. |
| private final long finishLsn; // If going backwards, read up to this LSN. |
| |
| /* For checking checksum on the read. */ |
| protected ChecksumValidator cksumValidator; |
| |
| private boolean doChecksumOnRead; // Validate checksums |
| private boolean alwaysValidateChecksum; // Validate for all entry types |
| |
| protected final Logger logger; |
| |
| public FileReader(EnvironmentImpl envImpl, |
| int readBufferSize, |
| boolean forward, |
| long startLsn, |
| Long singleFileNumber, |
| long endOfFileLsn, |
| long finishLsn) |
| throws DatabaseException { |
| |
| this(envImpl, readBufferSize, forward, |
| startLsn, singleFileNumber, endOfFileLsn, finishLsn, |
| envImpl.getLogManager().getChecksumOnRead()); |
| } |
| |
| public FileReader(EnvironmentImpl envImpl, |
| int readBufferSize, |
| boolean forward, |
| long startLsn, |
| Long singleFileNumber, |
| long endOfFileLsn, |
| long finishLsn, |
| boolean doChecksumOnRead) |
| throws DatabaseException { |
| |
| this.envImpl = envImpl; |
| this.fileManager = envImpl.getFileManager(); |
| this.singleFile = (singleFileNumber != null); |
| this.forward = forward; |
| |
| this.doChecksumOnRead = doChecksumOnRead; |
| if (this.doChecksumOnRead) { |
| cksumValidator = new ChecksumValidator(envImpl); |
| } |
| |
| window = makeWindow(readBufferSize); |
| saveBuffer = ByteBuffer.allocate(readBufferSize); |
| |
| /* stats */ |
| nRead = 0; |
| |
| /* Determine the starting position. */ |
| this.startLsn = startLsn; |
| this.finishLsn = finishLsn; |
| |
| logger = envImpl.getLogger(); |
| |
| initStartingPosition(endOfFileLsn, singleFileNumber); |
| } |
| |
| /** |
| * May be overridden by other FileReaders. |
| * @throws DatabaseException |
| */ |
| protected ReadWindow makeWindow(int readBufferSize) |
| throws DatabaseException { |
| |
| return new ReadWindow(readBufferSize, envImpl); |
| } |
| |
| /** |
| * Helper for determining the starting position and opening up a file at |
| * the desired location. |
| */ |
| protected void initStartingPosition(long endOfFileLsn, |
| Long ignoreSingleFileNumber) { |
| eof = false; |
| if (forward) { |
| |
| /* |
| * Start off at the startLsn. If that's null, start at the |
| * beginning of the log. If there are no log files, set eof. |
| */ |
| if (startLsn != DbLsn.NULL_LSN) { |
| window.initAtFileStart(startLsn); |
| } else { |
| Long firstNum = fileManager.getFirstFileNum(); |
| if (firstNum == null) { |
| eof = true; |
| } else { |
| window.initAtFileStart(DbLsn.makeLsn(firstNum, 0)); |
| } |
| } |
| |
| /* |
| * After we read the first entry, the currentEntry will point here. |
| */ |
| nextEntryOffset = window.getEndOffset(); |
| } else { |
| |
| /* |
| * Make the read buffer look like it's positioned off the end of |
| * the file. Initialize the first LSN we want to read. When |
| * traversing the log backwards, we always start at the very end. |
| */ |
| assert startLsn != DbLsn.NULL_LSN; |
| window.initAtFileStart(endOfFileLsn); |
| |
| /* |
| * currentEntryPrevOffset points to the entry we want to start out |
| * reading when going backwards. If it's 0, the entry we want to |
| * read is in a different file. |
| */ |
| if (DbLsn.getFileNumber(startLsn) == |
| DbLsn.getFileNumber(endOfFileLsn)) { |
| currentEntryPrevOffset = DbLsn.getFileOffset(startLsn); |
| } else { |
| currentEntryPrevOffset = 0; |
| } |
| currentEntryOffset = DbLsn.getFileOffset(endOfFileLsn); |
| } |
| } |
| |
| /** |
| * Whether to always validate the checksum, even for non-target entries. |
| */ |
| public void setAlwaysValidateChecksum(boolean validate) { |
| alwaysValidateChecksum = validate; |
| } |
| |
| /** |
| * @return the number of entries processed by this reader. |
| */ |
| public int getNumRead() { |
| return nRead; |
| } |
| |
| public long getNRepeatIteratorReads() { |
| return window.getNRepeatIteratorReads(); |
| } |
| |
| /** |
| * Get LSN of the last entry read. |
| */ |
| public long getLastLsn() { |
| return DbLsn.makeLsn(window.currentFileNum(), currentEntryOffset); |
| } |
| |
| /** |
| * Returns the VLSN of the last entry read, or null if the entry has no |
| * VLSN (not replicated and has no migrated VLSN). |
| */ |
| public VLSN getLastVlsn() { |
| return currentEntryHeader.getVLSN(); |
| } |
| |
| /** |
| * Returns the total size (including header) of the last entry read. |
| */ |
| public int getLastEntrySize() { |
| return currentEntryHeader.getEntrySize(); |
| } |
| |
| /** |
| * Returns the log version of the last entry read. |
| */ |
| public int getLogVersion() { |
| return currentEntryHeader.getVersion(); |
| } |
| |
| /** |
| * Scans the log files until either it has reached the end of the log or |
| * has hit an invalid portion. |
| * |
| * @return true if an element has been read, false at end-of-log. |
| * |
| * @throws EnvironmentFailureException if a ChecksumException, |
| * FileNotFoundException, or another internal problem occurs. |
| */ |
| public boolean readNextEntry() { |
| try { |
| return readNextEntryAllowExceptions(); |
| } catch (FileNotFoundException e) { |
| throw new EnvironmentFailureException( |
| envImpl, EnvironmentFailureReason.LOG_FILE_NOT_FOUND, e); |
| } catch (ChecksumException e) { |
| throw new EnvironmentFailureException( |
| envImpl, EnvironmentFailureReason.LOG_CHECKSUM, e); |
| } |
| } |
| |
| /** |
| * Variant of readNextEntry that throws FileNotFoundException and |
| * ChecksumException, rather than wrapping them in an |
| * EnvironmentFailureException and invalidating the enviornment. This |
| * allows users of this class (see cleaner.FileProcessor), and subclasses |
| * that override readNextEntry (see ScavengerFileReader and |
| * LastFileReader), to handle these exceptions specially. |
| */ |
| public final boolean readNextEntryAllowExceptions() |
| throws FileNotFoundException, ChecksumException { |
| |
| boolean foundEntry = false; |
| long savedCurrentEntryOffset = currentEntryOffset; |
| long savedNextEntryOffset = nextEntryOffset; |
| |
| try { |
| while ((!eof) && (!foundEntry)) { |
| |
| /* Read the invariant portion of the next header. */ |
| getLogEntryInReadBuffer(); |
| ByteBuffer dataBuffer = |
| readData(LogEntryHeader.MIN_HEADER_SIZE, |
| true); // collectData |
| |
| readBasicHeader(dataBuffer); |
| |
| boolean isTarget; |
| boolean isChecksumTarget; |
| boolean collectData; |
| |
| if (currentEntryHeader.isVariableLength()) { |
| |
| /* |
| * For all variable length entries, init the checksum w/the |
| * invariant portion of the header, before we know whether |
| * the entry is a target for this reader. This has |
| * to be done before we read the variable portion of the |
| * header, because readData() only guarantees that it |
| * returns a dataBuffer that contains the next bytes that |
| * are needed, and has no guarantee that it holds any bytes |
| * that were previously read. The act of calling |
| * readData() to obtain the optional portion may reset the |
| * dataBuffer, and nudge the invariant part of the header |
| * out of the buffer returned by readData() |
| */ |
| if (currentEntryHeader.hasChecksum()) { |
| startChecksum(dataBuffer); |
| } |
| |
| int optionalPortionLen = |
| currentEntryHeader.getVariablePortionSize(); |
| |
| /* Load the optional part of the header into a buffer. */ |
| dataBuffer = readData(optionalPortionLen, true); |
| |
| /* |
| * Add to checksum while the buffer is positioned at |
| * the start of the new bytes. |
| */ |
| if (currentEntryHeader.hasChecksum()) { |
| addToChecksum(dataBuffer, optionalPortionLen); |
| } |
| |
| /* Now read the optional bytes. */ |
| currentEntryHeader.readVariablePortion(dataBuffer); |
| } |
| |
| /* |
| * We've read the header of the next logrec. Move up our |
| * offsets if we're moving forward. If we're moving |
| * backwards, we set our offset before we read the header, |
| * because we knew where the entry started. |
| */ |
| if (forward) { |
| currentEntryOffset = nextEntryOffset; |
| nextEntryOffset += |
| currentEntryHeader.getSize() + // header size |
| currentEntryHeader.getItemSize(); // item size |
| } |
| |
| try { |
| isTarget = isTargetEntry(); |
| |
| isChecksumTarget = currentEntryHeader.hasChecksum() && |
| (isTarget || alwaysValidateChecksum); |
| |
| if (!currentEntryHeader.isVariableLength()) { |
| startChecksum(dataBuffer, isChecksumTarget); |
| } |
| |
| collectData = |
| (isChecksumTarget && doChecksumOnRead) || isTarget; |
| |
| /* |
| * Read in the body of the next entry. Note that even if |
| * this isn't a targeted entry, we have to move the buffer |
| * position along. |
| */ |
| dataBuffer = readData(currentEntryHeader.getItemSize(), |
| collectData); |
| } catch (Throwable e) { |
| if (forward) { |
| currentEntryOffset = savedCurrentEntryOffset; |
| nextEntryOffset = savedNextEntryOffset; |
| } |
| throw e; |
| } |
| |
| /* Validate the log entry checksum. */ |
| validateChecksum(dataBuffer, isChecksumTarget); |
| |
| if (isTarget) { |
| |
| /* |
| * For a target entry, call the subclass reader's |
| * processEntry method to do whatever we need with the |
| * entry. It returns true if this entry is one that should |
| * be returned. Note that some entries, although targeted |
| * and read, are not returned. |
| */ |
| if (processEntry(dataBuffer)) { |
| foundEntry = true; |
| nRead++; |
| } |
| } else if (collectData) { |
| |
| /* |
| * For a non-target entry that was validated, the buffer is |
| * positioned at the start of the entry; skip over it. |
| */ |
| skipEntry(dataBuffer); |
| } |
| } |
| } catch (EOFException e) { |
| eof = true; |
| } catch (DatabaseException e) { |
| eof = true; |
| /* Report on error. */ |
| reportProblem(e); |
| throw e; |
| } |
| |
| return foundEntry; |
| } |
| |
| /** |
| * May be called by processEntry when it determines that the entry does not |
| * need to be read/de-serialized. |
| */ |
| protected void skipEntry(ByteBuffer entryBuffer) { |
| entryBuffer.position( |
| entryBuffer.position() + |
| currentEntryHeader.getItemSize()); |
| } |
| |
| private void reportProblem(Exception e) { |
| |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Halted log file reading at file 0x"). |
| append(Long.toHexString(window.currentFileNum())). |
| append(" offset 0x"). |
| append(Long.toHexString(nextEntryOffset)). |
| append(" offset(decimal)="). |
| append(nextEntryOffset). |
| append(" prev=0x"). |
| append(Long.toHexString(currentEntryPrevOffset)); |
| |
| if (currentEntryHeader != null) { |
| LogEntryType problemType = |
| LogEntryType.findType(currentEntryHeader.getType()); |
| sb.append(":\nentry="). |
| append(problemType). |
| append("type="). |
| append(currentEntryHeader.getType()). |
| append(",version="). |
| append(currentEntryHeader.getVersion()). |
| append(")\nprev=0x"). |
| append(Long.toHexString(currentEntryPrevOffset)). |
| append("\nsize="). |
| append(currentEntryHeader.getItemSize()). |
| append("\nNext entry should be at 0x"). |
| append(Long.toHexString(nextEntryOffset + |
| currentEntryHeader.getSize() + |
| currentEntryHeader.getItemSize())); |
| } |
| |
| LoggerUtils.traceAndLogException |
| (envImpl, "FileReader", "readNextEntry", sb.toString(), e); |
| } |
| |
| /** |
| * Make sure that the start of the target log entry is in the header. |
| */ |
| private void getLogEntryInReadBuffer() |
| throws ChecksumException, |
| EOFException, |
| FileNotFoundException, |
| DatabaseException { |
| |
| /* |
| * If we're going forward, because we read every byte sequentially, |
| * we're always sure the read buffer is positioned at the right spot. |
| * If we go backwards, we need to jump the buffer position. These |
| * methods may be overridden by subclasses. |
| */ |
| if (forward) { |
| setForwardPosition(); |
| } else { |
| setBackwardPosition(); |
| } |
| } |
| |
| /** |
| * Ensure that the next target is in the window. The default behavior is |
| * that the next target is the next, following entry, so we can assume that |
| * it's in the window. All we have to do is to check if we've gone past |
| * the specified end point. |
| * @throws DatabaseException |
| * @throws FileNotFoundException |
| * @throws ChecksumException |
| */ |
| protected void setForwardPosition() |
| throws EOFException, |
| DatabaseException, |
| ChecksumException, |
| FileNotFoundException { |
| |
| if (finishLsn != DbLsn.NULL_LSN) { |
| /* The next log entry has passed the end LSN. */ |
| long nextLsn = DbLsn.makeLsn(window.currentFileNum(), |
| nextEntryOffset); |
| if (DbLsn.compareTo(nextLsn, finishLsn) >= 0) { |
| throw new EOFException(); |
| } |
| } |
| } |
| |
| /** |
| * Ensure that the next target is in the window. The default behavior is |
| * that the next target is the next previous entry. |
| * @throws DatabaseException |
| */ |
| protected void setBackwardPosition() |
| throws ChecksumException, |
| FileNotFoundException, |
| EOFException, |
| DatabaseException { |
| |
| /* |
| * currentEntryPrevOffset is the entry before the current entry. |
| * currentEntryOffset is the entry we just read (or the end of the |
| * file if we're starting out. |
| */ |
| if ((currentEntryPrevOffset != 0) && |
| window.containsOffset(currentEntryPrevOffset)) { |
| |
| /* The next log entry has passed the start LSN. */ |
| long nextLsn = DbLsn.makeLsn(window.currentFileNum(), |
| currentEntryPrevOffset); |
| if (finishLsn != DbLsn.NULL_LSN) { |
| if (DbLsn.compareTo(nextLsn, finishLsn) == -1) { |
| throw new EOFException("finish=" + |
| DbLsn.getNoFormatString(finishLsn) + |
| "next=" + |
| DbLsn.getNoFormatString(nextLsn)); |
| } |
| } |
| |
| /* This log entry starts in this buffer, just reposition. */ |
| window.positionBuffer(currentEntryPrevOffset); |
| } else { |
| |
| /* |
| * The start of the log entry is not in this read buffer so |
| * we must fill the buffer again. |
| * |
| * 1) The target log entry is in a different file from the |
| * current window's file. Move the window to the previous |
| * file and start the read from the target LSN. |
| * |
| * 2) The target log entry is the same file but the log entry |
| * is larger than the read chunk size. Start the next read |
| * buffer from the target LSN. It's going to take multiple |
| * reads to get the log entry, and we might as well get as |
| * much as possible. |
| * |
| * 3) In the same file, and the log entry fits within one |
| * read buffer. Try to position the next buffer chunk so the |
| * target entry is held within the buffer, all the way at the |
| * end. That way, since we're reading backwards, there will be |
| * more buffered data available for following reads. |
| */ |
| long nextFile; |
| long nextWindowStart; |
| long nextTarget; |
| |
| if (currentEntryPrevOffset == 0) { |
| /* Case 1: Go to another file. */ |
| currentEntryPrevOffset = fileManager.getFileHeaderPrevOffset |
| (window.currentFileNum()); |
| |
| Long prevFileNum = |
| fileManager.getFollowingFileNum(window.currentFileNum(), |
| false); |
| if (prevFileNum == null) { |
| throw new EOFException("No file following " + |
| window.currentFileNum()); |
| } |
| |
| /* |
| * Check finishLSN before proceeding, in case we should stop |
| * the search before attempting to set the file reader to a |
| * position in the previous file. In [#22407] we threw a |
| * spurious EFE complaining that we cannot read backwards over |
| * a cleaned file because the previous file had been cleaned |
| * away. |
| */ |
| if (finishLsn != DbLsn.NULL_LSN && |
| prevFileNum < DbLsn.getFileNumber(finishLsn)) { |
| throw new EOFException( |
| "finish=" + DbLsn.getNoFormatString(finishLsn) + |
| " nextFile=0x" + Long.toHexString(prevFileNum)); |
| } |
| |
| if (window.currentFileNum() - prevFileNum.longValue() != 1) { |
| handleGapInBackwardsScan(prevFileNum); |
| } |
| |
| nextFile = prevFileNum; |
| nextWindowStart = currentEntryPrevOffset; |
| nextTarget = currentEntryPrevOffset; |
| } else if ((currentEntryOffset - currentEntryPrevOffset) > |
| window.capacity()) { |
| |
| /* |
| * Case 2: The entry is in the same file, but is bigger |
| * than one buffer. Position it at the front of the buffer. |
| */ |
| nextFile = window.currentFileNum(); |
| nextWindowStart = currentEntryPrevOffset; |
| nextTarget = currentEntryPrevOffset; |
| } else { |
| |
| /* |
| * Case 3: In same file, but not in this buffer. The target |
| * entry will fit in one buffer. |
| */ |
| nextFile = window.currentFileNum(); |
| long newPosition = currentEntryOffset - |
| window.capacity(); |
| nextWindowStart = (newPosition < 0) ? 0 : newPosition; |
| nextTarget = currentEntryPrevOffset; |
| } |
| |
| /* The next log entry has passed the start LSN. */ |
| long nextLsn = DbLsn.makeLsn(nextFile, |
| currentEntryPrevOffset); |
| if (finishLsn != DbLsn.NULL_LSN) { |
| if (DbLsn.compareTo(nextLsn, finishLsn) == -1) { |
| throw new EOFException("finish=" + |
| DbLsn.getNoFormatString(finishLsn) + |
| " next=" + |
| DbLsn.getNoFormatString(nextLsn)); |
| } |
| } |
| |
| window.slideAndFill |
| (nextFile, nextWindowStart, nextTarget, forward); |
| } |
| |
| /* The current entry will start at this offset. */ |
| currentEntryOffset = currentEntryPrevOffset; |
| } |
| |
| /** |
| * Read the basic log entry header, leaving the buffer mark at the |
| * beginning of the checksummed header data. |
| */ |
| private void readBasicHeader(ByteBuffer dataBuffer) |
| throws ChecksumException, DatabaseException { |
| |
| /* Read the header for this entry. */ |
| currentEntryHeader = new LogEntryHeader( |
| dataBuffer, window.logVersion, window.getCurrentLsn()); |
| |
| /* |
| * currentEntryPrevOffset is a separate field, and is not obtained |
| * directly from the currentEntryHeader, because it is initialized and |
| * used before any log entry was read. |
| */ |
| currentEntryPrevOffset = currentEntryHeader.getPrevOffset(); |
| } |
| |
| /** |
| * Reset the checksum validator and add the new header bytes. Assumes that |
| * the data buffer is positioned just past the end of the invariant |
| * portion of the log entry header. |
| * @throws DatabaseException |
| */ |
| private void startChecksum(ByteBuffer dataBuffer) |
| throws ChecksumException { |
| |
| startChecksum(dataBuffer, true /* isChecksumTarget */); |
| } |
| |
| private void startChecksum(ByteBuffer dataBuffer, |
| boolean isChecksumTarget) |
| throws ChecksumException { |
| |
| if (!doChecksumOnRead) { |
| return; |
| } |
| |
| if (!isChecksumTarget) { |
| return; |
| } |
| |
| /* Clear out any previous data. */ |
| cksumValidator.reset(); |
| |
| int originalPosition = dataBuffer.position(); |
| if (currentEntryHeader.isInvisible()) { |
| |
| /* |
| * Turn off invisibility so that the checksum will succeed. When |
| * entries are made invisible, the checksum is not adjusted. Note |
| * that the dataBuffer can leave the invisible bit transformed, |
| * because the header has already been initialized, and this data |
| * will never be read again. |
| */ |
| LogEntryHeader.turnOffInvisible(dataBuffer, originalPosition - |
| LogEntryHeader.MIN_HEADER_SIZE); |
| } |
| |
| /* Position the buffer at the start of the data, after the checksum. */ |
| int headerSizeMinusChecksum = |
| currentEntryHeader.getInvariantSizeMinusChecksum(); |
| int entryTypeStart = originalPosition - headerSizeMinusChecksum; |
| dataBuffer.position(entryTypeStart); |
| |
| /* Load the validate with the header bytes. */ |
| cksumValidator.update(dataBuffer, headerSizeMinusChecksum); |
| |
| /* Move the data buffer back to the original position. */ |
| dataBuffer.position(originalPosition); |
| } |
| |
| private void addToChecksum(ByteBuffer dataBuffer, int length) |
| throws ChecksumException { |
| |
| if (!doChecksumOnRead) { |
| return; |
| } |
| |
| cksumValidator.update(dataBuffer, length); |
| } |
| |
| /** |
| * Add the entry bytes to the checksum and check the value. This method |
| * must be called with the buffer positioned at the start of the entry. |
| */ |
| private void validateChecksum(ByteBuffer dataBuffer, |
| boolean isChecksumTarget) |
| throws ChecksumException { |
| |
| if (!doChecksumOnRead) { |
| return; |
| } |
| |
| if (!isChecksumTarget) { |
| return; |
| } |
| |
| cksumValidator.update(dataBuffer, currentEntryHeader.getItemSize()); |
| cksumValidator.validate(currentEntryHeader.getChecksum(), |
| window.currentFileNum(), |
| currentEntryOffset); |
| } |
| |
| /** |
| * Try to read a specified number of bytes. |
| * @param amountToRead is the number of bytes we need |
| * @param collectData is true if we need to actually look at the data. |
| * If false, we know we're skipping this entry, and all we need to |
| * do is to count until we get to the right spot. |
| * @return a byte buffer positioned at the head of the desired portion, |
| * or null if we reached eof. |
| */ |
| private ByteBuffer readData(int amountToRead, boolean collectData) |
| throws ChecksumException, |
| EOFException, |
| FileNotFoundException, |
| DatabaseException { |
| |
| int alreadyRead = 0; |
| ByteBuffer completeBuffer = null; |
| saveBuffer.clear(); |
| |
| while ((alreadyRead < amountToRead) && !eof) { |
| |
| int bytesNeeded = amountToRead - alreadyRead; |
| if (window.hasRemaining()) { |
| |
| /* There's data in the window, process it. */ |
| if (collectData) { |
| |
| /* |
| * Save data in a buffer for processing. |
| */ |
| if ((alreadyRead > 0) || |
| (window.remaining() < bytesNeeded)) { |
| |
| /* We need to piece an entry together. */ |
| copyToSaveBuffer(bytesNeeded); |
| alreadyRead = saveBuffer.position(); |
| completeBuffer = saveBuffer; |
| } else { |
| |
| /* A complete entry is available in this buffer. */ |
| completeBuffer = window.getBuffer(); |
| alreadyRead = amountToRead; |
| } |
| } else { |
| |
| /* |
| * We're not processing the data, so need to save it. just |
| * move buffer positions. |
| */ |
| int positionIncrement = |
| (window.remaining() > bytesNeeded) ? |
| bytesNeeded : window.remaining(); |
| |
| alreadyRead += positionIncrement; |
| window.incrementBufferPosition(positionIncrement); |
| completeBuffer = window.getBuffer(); |
| } |
| } else { |
| |
| /* |
| * Look for more data. |
| */ |
| if (window.fillNext(singleFile, bytesNeeded)) { |
| /* This call to fillNext slid the window to a new file. */ |
| nextEntryOffset = 0; |
| } |
| } |
| } |
| |
| /* Flip the save buffer just in case we've been accumulating in it. */ |
| saveBuffer.flip(); |
| |
| return completeBuffer; |
| } |
| |
| /* Try to skip over a specified number of bytes. */ |
| public void skipData(int amountToSkip) |
| throws ChecksumException, |
| EOFException, |
| FileNotFoundException, |
| DatabaseException { |
| |
| try { |
| readData(amountToSkip, false); |
| } catch (DatabaseException e) { |
| reportProblem(e); |
| throw e; |
| } |
| } |
| |
| /** |
| * Copy the required number of bytes into the save buffer. |
| */ |
| private void copyToSaveBuffer(int bytesNeeded) { |
| /* How much can we get from this current read buffer? */ |
| int bytesFromThisBuffer; |
| |
| if (bytesNeeded <= window.remaining()) { |
| bytesFromThisBuffer = bytesNeeded; |
| } else { |
| bytesFromThisBuffer = window.remaining(); |
| } |
| |
| /* Gather it all into this save buffer. */ |
| ByteBuffer temp; |
| |
| /* Make sure the save buffer is big enough. */ |
| if (saveBuffer.capacity() - saveBuffer.position() < |
| bytesFromThisBuffer) { |
| /* Grow the save buffer. */ |
| temp = ByteBuffer.allocate(saveBuffer.capacity() + |
| bytesFromThisBuffer); |
| saveBuffer.flip(); |
| temp.put(saveBuffer); |
| saveBuffer = temp; |
| } |
| |
| /* |
| * Bulk copy only the required section from the read buffer into the |
| * save buffer. We need from readBuffer.position() to |
| * readBuffer.position() + bytesFromThisBuffer |
| */ |
| temp = window.getBuffer().slice(); |
| temp.limit(bytesFromThisBuffer); |
| saveBuffer.put(temp); |
| window.incrementBufferPosition(bytesFromThisBuffer); |
| } |
| |
| /** |
| * Returns the number of reads since the last time this method was called. |
| */ |
| public int getAndResetNReads() { |
| return window.getAndResetNReads(); |
| } |
| |
| /** |
| * This method is called by readNextEntry() after the header of the current |
| * logrec has been de-serialized, but not the body. Based on header info |
| * only, it may perform some actions and then decide whether the rest of |
| * the logrec should be de-serialized or just skipped. |
| * |
| * @return true if this reader should process the current logrec further, |
| * via the processEntry() method. A logrec must be passed to processEntry |
| * if the full logrec (not just the header) must be de-serialized for |
| * further processing. Return false if no further processing is needed, |
| * in which case the current logrec will be skipped (i.e, not returned |
| * to the caller of readNextEntry(). |
| * |
| * @throws DatabaseException from subclasses. |
| */ |
| protected boolean isTargetEntry() |
| throws DatabaseException { |
| |
| return true; |
| } |
| |
| /** |
| * Each file reader implements this method to process the entry data. |
| * |
| * @param entryBuffer A ByteBuffer that the logrec data and is positioned |
| * at the start of the logrec body (i.e., just after the logrec header). |
| * |
| * @return true if this entry should be returned to the caller of |
| * readNextEntry(). |
| */ |
| protected abstract boolean processEntry(ByteBuffer entryBuffer) |
| throws DatabaseException; |
| |
| /** |
| * Never seen by user, used to indicate that the file reader should stop. |
| */ |
| @SuppressWarnings("serial") |
| public static class EOFException extends Exception { |
| public EOFException() { |
| super(); |
| } |
| |
| /* |
| * @param message The message is used to hold debugging |
| * information. |
| */ |
| public EOFException(String message) { |
| super(message); |
| } |
| } |
| |
| /** |
| * @return true if the current entry is part of replication stream. |
| */ |
| public boolean entryIsReplicated() { |
| |
| if (currentEntryHeader == null) { |
| throw EnvironmentFailureException.unexpectedState |
| ("entryIsReplicated should not be used before reader is " + |
| "initialized"); |
| } |
| return currentEntryHeader.getReplicated(); |
| } |
| |
| /** |
| * TBW |
| */ |
| protected void handleGapInBackwardsScan(long prevFileNum) { |
| throw new EnvironmentFailureException |
| (envImpl, |
| EnvironmentFailureReason.LOG_INTEGRITY, |
| "Cannot read backward over cleaned file" + |
| " from 0x" + Long.toHexString(window.currentFileNum()) + |
| " to 0x" + Long.toHexString(prevFileNum)); |
| } |
| |
| /** |
| * A ReadWindow provides a swath of data read from the JE log. |
| */ |
| protected static class ReadWindow { |
| |
| /* |
| * fileNum, startOffset and endOffset indicate how the read buffer maps |
| * to the JE log. For example, if the read buffer size is 200 and the |
| * read buffer was filled from file 9, starting at byte 100, then: |
| * fileNum = 9 |
| * startOffset = 100 |
| * endOffset = 300 |
| * Note that the end point is not inclusive; endOffset is > the |
| * readBuffer's end. |
| */ |
| private long fileNum; // file number we're pointing to |
| private int logVersion; // log version for fileNum/readBuffer |
| protected long startOffset;// file offset that maps to buf start |
| protected long endOffset; // file offset that maps to buf end |
| protected ByteBuffer readBuffer; // buffer for reading from the file |
| |
| /* read buffer can't grow larger than this */ |
| private final int maxReadBufferSize; |
| |
| protected final EnvironmentImpl envImpl; |
| protected final FileManager fileManager; |
| |
| /* |
| * The number of times we've tried to read in a log entry that was too |
| * large for the read buffer. |
| */ |
| private long nRepeatIteratorReads; |
| |
| /* Number of reads since the last time getAndResetNReads was called. */ |
| private int nReadOperations; |
| |
| protected ReadWindow(int readBufferSize, EnvironmentImpl envImpl) { |
| DbConfigManager configManager = envImpl.getConfigManager(); |
| maxReadBufferSize = |
| configManager.getInt(EnvironmentParams.LOG_ITERATOR_MAX_SIZE); |
| this.envImpl = envImpl; |
| fileManager = envImpl.getFileManager(); |
| |
| readBuffer = ByteBuffer.allocate(readBufferSize); |
| readBuffer.flip(); |
| } |
| |
| /* |
| * Position this window at this LSN, but leave it empty, it has no data |
| * yet. |
| */ |
| public void initAtFileStart(long startLsn) { |
| setFileNum(DbLsn.getFileNumber(startLsn), |
| LogEntryType.UNKNOWN_FILE_HEADER_VERSION); |
| startOffset = DbLsn.getFileOffset(startLsn); |
| endOffset = startOffset; |
| } |
| |
| public long getEndOffset() { |
| return endOffset; |
| } |
| |
| /** |
| * Ensure that whenever we change the fileNum, the logVersion is also |
| * updated. The fileNum and logVersion fields should be kept private. |
| */ |
| protected void setFileNum(final long fileNum, final int logVersion) { |
| this.fileNum = fileNum; |
| this.logVersion = logVersion; |
| } |
| |
| public long currentFileNum() { |
| return fileNum; |
| } |
| |
| /* Return true if this offset is contained with the readBuffer. */ |
| boolean containsOffset(long targetOffset) { |
| return (targetOffset >= startOffset) && |
| (targetOffset < endOffset); |
| } |
| |
| /* Return true if this lsn is contained with the readBuffer. */ |
| public boolean containsLsn(long targetFileNumber, long targetOffset) { |
| return ((fileNum == targetFileNumber) && |
| containsOffset(targetOffset)); |
| } |
| |
| /* Position the readBuffer to the targetOffset. */ |
| public void positionBuffer(long targetOffset) { |
| |
| assert containsOffset(targetOffset) : this + " doesn't contain " + |
| DbLsn.getNoFormatString(targetOffset); |
| |
| readBuffer.position((int) (targetOffset - startOffset)); |
| } |
| |
| /* Move the readBuffer position up by the given increment. */ |
| void incrementBufferPosition(int increment) { |
| int currentPosition = readBuffer.position(); |
| readBuffer.position(currentPosition + increment); |
| } |
| |
| /* |
| * Reposition to the specified file, and fill starting at |
| * startOffset. Position the window's buffer to point at the log entry |
| * indicated by targetOffset |
| */ |
| public void slideAndFill(long windowfileNum, |
| long windowStartOffset, |
| long targetOffset, |
| boolean forward) |
| throws ChecksumException, |
| FileNotFoundException, |
| DatabaseException { |
| |
| FileHandle fileHandle = fileManager.getFileHandle(windowfileNum); |
| try { |
| startOffset = windowStartOffset; |
| setFileNum(windowfileNum, fileHandle.getLogVersion()); |
| boolean foundData = fillFromFile(fileHandle, targetOffset); |
| |
| /* |
| * When reading backwards, we need to guarantee there is no log |
| * gap, throws out an EnvironmentFailreException if it exists. |
| */ |
| if (!foundData && !forward) { |
| throw EnvironmentFailureException.unexpectedState |
| ("Detected a log file gap when reading backwards. " + |
| "Target position = " + DbLsn.getNoFormatString |
| (DbLsn.makeLsn(windowfileNum, targetOffset)) + |
| " starting position = " + DbLsn.getNoFormatString |
| (DbLsn.makeLsn(windowfileNum, windowStartOffset)) + |
| " end position = " + DbLsn.getNoFormatString |
| (DbLsn.makeLsn(windowfileNum, endOffset))); |
| } |
| } finally { |
| fileHandle.release(); |
| } |
| } |
| |
| /** |
| * Fill up the read buffer with more data, moving along to the |
| * following file (next largest number) if needed. |
| * @return true if the fill moved us to a new file. |
| */ |
| protected boolean fillNext(boolean singleFile, int bytesNeeded) |
| throws ChecksumException, |
| FileNotFoundException, |
| EOFException, |
| DatabaseException { |
| |
| adjustReadBufferSize(bytesNeeded); |
| |
| FileHandle fileHandle = null; |
| try { |
| /* Get a file handle to read in more log. */ |
| fileHandle = fileManager.getFileHandle(fileNum); |
| |
| /* |
| * Check to see if we've come to the end of the file. If so, |
| * get the next file. |
| */ |
| startOffset = endOffset; |
| if (fillFromFile(fileHandle, startOffset)) { |
| /* |
| * Successfully filled the read buffer, but didn't move to |
| * a new file. |
| */ |
| return false; |
| } |
| |
| /* This file is done -- can we read in the next file? */ |
| if (singleFile) { |
| throw new EOFException("Single file only"); |
| } |
| |
| Long nextFile = |
| fileManager.getFollowingFileNum(fileNum, |
| true /* forward */); |
| |
| if (nextFile == null) { |
| throw new EOFException(); |
| } |
| |
| fileHandle.release(); |
| fileHandle = null; |
| /* |
| * TODO: This doesn't work if file is deleted after calling |
| * getFollowingFileNum above and calling getFileHandle below. |
| * That can happen to due to deletion of reserved files by the |
| * cleaner or eraser. We need a retry loop here that catches |
| * FileNotFoundException and calls getFollowingFileNum again. |
| * The retry method probably belongs in FileManager. |
| */ |
| fileHandle = fileManager.getFileHandle(nextFile); |
| setFileNum(nextFile, fileHandle.getLogVersion()); |
| startOffset = 0; |
| fillFromFile(fileHandle, 0); |
| return true; |
| } finally { |
| if (fileHandle != null) { |
| fileHandle.release(); |
| } |
| } |
| } |
| |
| /* |
| * Assume that the window is properly positioned. Try to fill the read |
| * buffer with data from this file handle, starting at the location |
| * indicated by the starting offset field. If this file contains more |
| * data, return true. If this file doesn't contain more data, return |
| * false. |
| * |
| * In all cases, leave the the read buffer pointing at the target |
| * offset and in a state that's ready to support reads, even if there |
| * is nothing in the buffer. Note that the target offset, which may not |
| * be the same as starting offset. |
| * @return true if more data was read, false if not. |
| */ |
| protected boolean fillFromFile(FileHandle fileHandle, |
| long targetOffset) |
| throws DatabaseException { |
| |
| boolean foundData = false; |
| readBuffer.clear(); |
| if (fileManager.readFromFile(fileHandle.getFile(), |
| readBuffer, |
| startOffset, |
| fileHandle.getFileNum(), |
| false /* dataKnownToBeInFile */)) { |
| foundData = true; |
| nReadOperations += 1; |
| /* |
| * Ensure that fileNum and logVersion are in sync. setFileNum |
| * handles changes in the file number. But we must also update |
| * the logVersion here to handle the first read after we |
| * initialize fileNum and logVersion is unknown. |
| */ |
| logVersion = fileHandle.getLogVersion(); |
| } |
| |
| /* |
| * In all cases, setup read buffer for valid reading. If the buffer |
| * has no data, it will be positioned at the beginning, and will be |
| * able to correctly return the fact that there is no data present. |
| */ |
| |
| endOffset = startOffset + readBuffer.position(); |
| readBuffer.flip(); |
| readBuffer.position((int) (targetOffset - startOffset)); |
| return foundData; |
| } |
| |
| /** |
| * Change the read buffer size if we start hitting large log entries so |
| * we don't get into an expensive cycle of multiple reads and piecing |
| * together of log entries. |
| */ |
| protected void adjustReadBufferSize(int amountToRead) { |
| |
| int readBufferSize = readBuffer.capacity(); |
| |
| /* |
| * We need to read something larger than the current buffer |
| * size. |
| */ |
| if (amountToRead > readBufferSize) { |
| |
| /* We're not at the max yet. */ |
| if (readBufferSize < maxReadBufferSize) { |
| |
| /* |
| * Make the buffer the minimum of amountToRead or a |
| * maxReadBufferSize. |
| */ |
| if (amountToRead < maxReadBufferSize) { |
| readBufferSize = amountToRead; |
| /* Make it a multiple of 1K. */ |
| int remainder = readBufferSize % 1024; |
| readBufferSize += 1024 - remainder; |
| readBufferSize = Math.min(readBufferSize, |
| maxReadBufferSize); |
| } else { |
| readBufferSize = maxReadBufferSize; |
| } |
| readBuffer = ByteBuffer.allocate(readBufferSize); |
| } |
| |
| if (amountToRead > readBuffer.capacity()) { |
| nRepeatIteratorReads++; |
| envImpl.getLogManager().incRepeatIteratorReads(); |
| } |
| } |
| } |
| |
| int capacity() { |
| return readBuffer.capacity(); |
| } |
| |
| int remaining() { |
| return readBuffer.remaining(); |
| } |
| |
| boolean hasRemaining() { |
| return readBuffer.hasRemaining(); |
| } |
| |
| ByteBuffer getBuffer() { |
| return readBuffer; |
| } |
| |
| /** |
| * Returns the number of reads since the last time this method was |
| * called. |
| */ |
| int getAndResetNReads() { |
| int tmp = nReadOperations; |
| nReadOperations = 0; |
| return tmp; |
| } |
| |
| long getNRepeatIteratorReads() { |
| return nRepeatIteratorReads; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| long start = DbLsn.makeLsn(fileNum, startOffset); |
| long end = DbLsn.makeLsn(fileNum, endOffset); |
| sb.append("window covers "); |
| sb.append(DbLsn.getNoFormatString(start)).append(" to "); |
| sb.append(DbLsn.getNoFormatString(end)); |
| sb.append(" positioned at "); |
| long target = getCurrentLsn(); |
| sb.append(DbLsn.getNoFormatString(target)); |
| return sb.toString(); |
| } |
| |
| long getCurrentLsn() { |
| return DbLsn.makeLsn(fileNum, startOffset + readBuffer.position()); |
| } |
| } |
| } |