/* * Bitronix Transaction Manager * * Copyright (c) 2011, Juergen Kellerer. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package bitronix.tm.journal.nio; import bitronix.tm.Configuration; import bitronix.tm.TransactionManagerServices; import bitronix.tm.journal.Journal; import bitronix.tm.journal.JournalRecord; import bitronix.tm.journal.MigratableJournal; import bitronix.tm.journal.ReadableJournal; import bitronix.tm.journal.nio.util.SequencedBlockingQueue; import bitronix.tm.utils.Uid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.util.*; import static bitronix.tm.journal.nio.NioJournalWritingThread.newRunningInstance; import static javax.transaction.Status.STATUS_COMMITTING; import static javax.transaction.Status.STATUS_ROLLING_BACK; /** * Nio & 'java.util.concurrent' based implementation of a transaction journal. * * @author juergen kellerer, 2011-04-30 * @see bitronix.tm.journal.Journal */ public class NioJournal implements Journal, MigratableJournal, ReadableJournal, NioJournalConstants { private static final Logger log = LoggerFactory.getLogger(NioJournal.class); private static final boolean trace = log.isTraceEnabled(); /** * Returns the journal file used by this implementation. * * @return the journal file used by this implementation. */ public static File getJournalFilePath() { Configuration config = TransactionManagerServices.getConfiguration(); File part1File = new File(config.getLogPart1Filename()); return new File(part1File.getParentFile(), "nio-" + part1File.getName()); } // Session tracking final NioTrackedTransactions trackedTransactions = new NioTrackedTransactions(); // Queueing & force related stuff final SequencedBlockingQueue<NioJournalFileRecord> pendingRecordsQueue = new SequencedBlockingQueue<NioJournalFileRecord>(); final NioForceSynchronizer forceSynchronizer = new NioForceSynchronizer(pendingRecordsQueue); // Worker volatile NioJournalWritingThread journalWritingThread; volatile File journalFilePath; volatile NioJournalFile journalFile; boolean skipForce = !TransactionManagerServices.getConfiguration().isForcedWriteEnabled(); boolean logOnlyMandatoryRecords = TransactionManagerServices.getConfiguration().isFilterLogStatus(); /** * {@inheritDoc} */ public void log(final int status, final Uid gtrid, Set<String> uniqueNames) throws IOException { assertJournalIsOpen(); if (gtrid == null) throw new IllegalArgumentException("GTRID cannot be set to null."); if (uniqueNames == null) uniqueNames = Collections.emptySet(); final NioJournalRecord record = new NioJournalRecord(status, gtrid, uniqueNames); if (logOnlyMandatoryRecords && !MANDATORY_STATUS_TO_LOG.contains(status)) { if (log.isDebugEnabled()) { log.debug("Journaling of non mandatory records is disabled. Skipping " + record); } return; } trackedTransactions.track(status, gtrid, record); if (trace) { log.trace("Attempting to log a new transaction log record " + record + "."); } try { final NioJournalFileRecord fileRecord = journalFile.createEmptyRecord(); record.encodeTo(fileRecord.createEmptyPayload(record.getRecordLength()), false); pendingRecordsQueue.putElement(fileRecord); } catch (InterruptedException e) { Thread.currentThread().interrupt(); IOException ioException = new InterruptedIOException(e.getMessage()); ioException.initCause(e); throw ioException; } } private void assertJournalIsOpen() throws IOException { if (!isOpen()) throw new IOException("The journal is not yet opened or was already closed."); } public boolean isSkipForce() { return skipForce; } public void setSkipForce(boolean skipForce) { if (isOpen()) throw new IllegalStateException("Cannot change skip force when the journal is already open."); this.skipForce = skipForce; } /** * Returns true if the journal is open. * * @return true if the journal is open. */ public final boolean isOpen() { return journalFile != null; } /** * {@inheritDoc} */ public synchronized void open() throws IOException { final boolean debug = log.isDebugEnabled(); journalFilePath = getJournalFilePath(); // HACK: Start - TODO: Resolve this! long journalSize = TransactionManagerServices.getConfiguration().getMaxLogSizeInMb() * 1024L * 1024L * 3L; // Default is 2, however 6mb seems to be the best trade-off between size and performance for this impl. // Configuration must be adjusted later to cover this correctly. // HACK: End if (debug) { log.debug("Attempting to open the journal file " + journalFilePath + " with a min fixed size of " + journalSize / 1024 / 1024 + "mb"); } if (trace) { log.trace("Calling close prior to open to ensure the journal wasn't opened before."); } close(); this.journalFile = new NioJournalFile(journalFilePath, journalSize); log.info("Successfully opened the journal file " + journalFilePath + "."); if (debug) { log.debug("Scanning for unfinished transactions within " + journalFilePath + "."); } for (NioJournalFileRecord fileRecord : journalFile.readAll(false)) { NioJournalRecord record = decodeFileRecord(fileRecord); if (record != null) { if (!record.isValid()) log.error("Transaction log entry " + record + " loaded from journal " + journalFilePath + " fails CRC32 check. Discarding the entry."); else trackedTransactions.track(record); } } log.info("Found " + trackedTransactions.size() + " unfinished transactions within the journal."); trackedTransactions.purgeTransactionsExceedingLifetime(); try { journalWritingThread = newRunningInstance(trackedTransactions, journalFile, isSkipForce() ? null : forceSynchronizer, pendingRecordsQueue); log.info("Successfully started a new log appender on the journal file " + journalFilePath + "."); } catch (InterruptedException e) { log.info("Interrupted the attempt to open the journal file " + journalFilePath + ". Will close the file now and " + "delegate the interrupt to the caller, letting it shutdown gracefully."); try { close(); throw new IOException("Failed to open journal file " + journalFilePath + " as the calling thread was interrupted."); } finally { Thread.currentThread().interrupt(); } } } private NioJournalRecord decodeFileRecord(NioJournalFileRecord fileRecord) { ByteBuffer buffer = fileRecord.getPayload(); try { buffer.mark(); return new NioJournalRecord(buffer, fileRecord.isValid()); } catch (Exception e) { buffer.reset(); String contentString = NioJournalFileRecord.bufferToString(buffer); log.error("Transaction log entry buffer with content <" + contentString + "> loaded from journal " + journalFilePath + " cannot be decoded. " + "Discarding the entry."); } return null; } /** * {@inheritDoc} */ public synchronized void close() throws IOException { closeLogAppender(); if (journalFile != null) { if (log.isDebugEnabled()) { log.debug("Attempting to close the nio transaction journal."); } journalFile.close(); journalFile = null; log.info("Closed the nio transaction journal."); } trackedTransactions.clear(); } private synchronized void closeLogAppender() throws IOException { if (journalWritingThread != null) { if (log.isDebugEnabled()) { log.debug("Attempting to close the nio log appender."); } journalWritingThread.shutdown(); journalWritingThread = null; } } /** * {@inheritDoc} */ public void shutdown() { try { log.info("Shutting down the nio transaction journal on " + journalFilePath + "."); close(); } catch (IOException e) { throw new RuntimeException(e); } } /** * {@inheritDoc} */ public void force() throws IOException { assertJournalIsOpen(); if (skipForce) return; if (!forceSynchronizer.waitOnEnlisted()) throw new IOException("Forced failed on the latest entry logged within this thread, see log output for more details."); } /** * {@inheritDoc} */ public Map<Uid, ? extends JournalRecord> collectDanglingRecords() throws IOException { assertJournalIsOpen(); final Map<Uid, NioJournalRecord> tracked = trackedTransactions.getTracked(); final Map<Uid, NioJournalRecord> dangling = new HashMap<Uid, NioJournalRecord>(tracked.size()); for (Map.Entry<Uid, NioJournalRecord> entry : tracked.entrySet()) { if (entry.getValue().getStatus() == STATUS_COMMITTING || entry.getValue().getStatus() == STATUS_ROLLING_BACK) dangling.put(entry.getKey(), entry.getValue()); } return dangling; } public void migrateTo(Journal other) throws IOException, IllegalArgumentException { if (other == this) throw new IllegalArgumentException("Cannot migrate a journal to itself (this == otherJournal)."); if (other == null) throw new IllegalArgumentException("The migration target journal may not be 'null'."); for (JournalRecord record : collectDanglingRecords().values()) other.log(record.getStatus(), record.getGtrid(), record.getUniqueNames()); } /** * {@inheritDoc} */ public synchronized void unsafeReadRecordsInto(Collection<JournalRecord> target, boolean includeInvalid) throws IOException { assertJournalIsOpen(); for (NioJournalFileRecord record : journalFile.readAll(includeInvalid)) { NioJournalRecord journalRecord = decodeFileRecord(record); if (journalRecord != null) target.add(journalRecord); } } /** * {@inheritDoc} */ @Override public String toString() { return "NioJournal{" + "journalFilePath=" + journalFilePath + ", skipForce=" + skipForce + ", trackedTransactions=" + trackedTransactions + ", forceSynchronizer=" + forceSynchronizer + ", journalWritingThread=" + journalWritingThread + ", journalFile=" + journalFile + '}'; } }