package io.eguan.dtx.journal; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * 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. * #L% */ import static io.eguan.dtx.journal.JournalFileUtils.getInverseBackupMap; import io.eguan.dtx.journal.JournalRotationManager.RotationEvent; import io.eguan.dtx.journal.JournalRotationManager.RotationListener; import java.io.File; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Iterator; import java.util.Map; import java.util.NavigableMap; import java.util.NoSuchElementException; import java.util.concurrent.Semaphore; import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Read-only representation of a transaction journal. * * @author oodrive * @author pwehrle * */ public class ReadOnlyTxJournal implements Iterable<JournalRecord> { private static final Logger LOGGER = LoggerFactory.getLogger(ReadOnlyTxJournal.class); private final File journalFile; private final JournalRotationManager rotationManager; /** * Constructs a read-only view of the given journal file. * * @param journalFile * the journal file to read, which must exist and be readable * @param rotationManager * the optional {@link JournalRotationManager} responsible for rotating the given journal file * @throws IllegalArgumentException * if the journal file does not exist or is not readable */ ReadOnlyTxJournal(@Nonnull final File journalFile, final JournalRotationManager rotationManager) throws IllegalArgumentException { if (!journalFile.exists() || !journalFile.canRead()) { throw new IllegalArgumentException("Journal file does not exist or is not readable; file=" + journalFile); } this.journalFile = journalFile; this.rotationManager = rotationManager; } @Override public final Iterator<JournalRecord> iterator() { final RoJournalIterator result = new RoJournalIterator(this.journalFile.toPath()); this.rotationManager.addRotationEventListener(result, this.journalFile.getAbsolutePath()); return result; } /** * {@link Iterator} over a {@link ReadOnlyTxJournal} that's capable of iterating over the entire journal, its backup * history included. * * */ private final class RoJournalIterator implements Iterator<JournalRecord>, RotationListener { private JournalRecord currentRecord; private final Path journalFile; private FileChannel journalChannel; private int lastBackupOffset; private long lastFilePosition; private final Semaphore switchLock = new Semaphore(1, true); private volatile boolean closed = false; private RoJournalIterator(final Path journalFile) { this.journalFile = journalFile; final NavigableMap<Integer, File> backupList = getInverseBackupMap(this.journalFile.getParent().toFile(), this.journalFile.getFileName().toString()); if (backupList.isEmpty()) { lastBackupOffset = 0; } else { final Integer backupIndex = backupList.firstKey(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Initializing backup index; backupRank=" + backupIndex); } lastBackupOffset = backupIndex.intValue(); } } @Override public final void rotationEventOccured(final RotationEvent rotevt) throws InterruptedException { if (closed) { return; } switch (rotevt.getStage()) { case PRE_ROTATE: if (LOGGER.isDebugEnabled()) { LOGGER.debug("Pre-rotate received; file=" + rotevt.getFilename()); } switchLock.acquire(); break; case ROTATE_SUCCESS: try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Rotation success received; file=" + rotevt.getFilename() + ", backupRank=" + lastBackupOffset); } lastBackupOffset++; journalChannel.close(); openReadFileChannel(); journalChannel.position(lastFilePosition); } catch (final IOException ie) { throw new IllegalStateException(ie); } finally { switchLock.release(); } break; case ROTATE_FAILURE: if (LOGGER.isDebugEnabled()) { LOGGER.debug("Rotation failure received; file=" + rotevt.getFilename() + ", backupRank=" + lastBackupOffset); } switchLock.release(); break; default: LOGGER.warn("Unhandled rotation event; file=" + rotevt.getFilename() + ",stage=" + rotevt.getStage()); } } @Override public final boolean hasNext() { final boolean result = (currentRecord != null) || readNextRecord(); if (!result) { closed = true; rotationManager.removeRotationEventListener(this); try { if (journalChannel != null) { journalChannel.close(); } } catch (final IOException e) { LOGGER.warn("Failed to close file channel", e); } } return result; } @Override public final JournalRecord next() { if ((currentRecord == null) && !readNextRecord()) { throw new NoSuchElementException(); } final JournalRecord result = currentRecord; readNextRecord(); return result; } @Override public final void remove() { throw new UnsupportedOperationException(); } private final boolean readNextRecord() { try { switchLock.acquire(); } catch (final InterruptedException e1) { throw new IllegalStateException("Interrupted"); } try { if (journalChannel == null || !journalChannel.isOpen()) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Channel closed on read, trying to re-open; backupIndex=" + lastBackupOffset); } openReadFileChannel(); if (!journalChannel.isOpen()) { return false; } } // checks if the end of a file was reached if (journalChannel.size() <= journalChannel.position()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("current file read to the end, switching; oldBackupRank=" + lastBackupOffset); } if (lastBackupOffset == 0) { currentRecord = null; return false; } journalChannel.close(); lastBackupOffset--; openReadFileChannel(); } currentRecord = JournalRecord.readRecordFromByteChannel(journalChannel); lastFilePosition = journalChannel.position(); return (currentRecord != null); } catch (final IOException e) { LOGGER.warn("Could not read from journal file", e); return false; } catch (final IllegalArgumentException ie) { LOGGER.warn("Could not read journal", ie); return false; } catch (final IllegalStateException ise) { LOGGER.warn("Journal unreadable", ise); return false; } finally { switchLock.release(); } } /** * (Re-)opens the read {@link FileChannel}. * * @throws IOException * if opening the file channel fails */ private final void openReadFileChannel() throws IOException { if (closed) { throw new IllegalStateException("Closed"); } if (lastBackupOffset == 0) { journalChannel = FileChannel.open(journalFile, StandardOpenOption.READ); } else { final Map<Integer, File> backupFileMap = getInverseBackupMap(journalFile.getParent().toFile(), journalFile.getFileName().toString()); final File targetFile = backupFileMap.get(Integer.valueOf(lastBackupOffset)); if (targetFile == null) { LOGGER.error("Target file not found; backupRank=" + lastBackupOffset + " fileList=" + backupFileMap.values()); throw new IllegalStateException("Target file does not exist; backupRank=" + lastBackupOffset); } journalChannel = FileChannel.open(targetFile.toPath(), StandardOpenOption.READ); } } } }