All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bitronix.tm.journal.DiskJournal Maven / Gradle / Ivy

There is a newer version: 62
Show newest version
/*
 * Copyright (C) 2006-2013 Bitronix Software (http://www.bitronix.be)
 *
 * 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.
 */
package bitronix.tm.journal;

import bitronix.tm.BitronixXid;
import bitronix.tm.Configuration;
import bitronix.tm.TransactionManagerServices;
import bitronix.tm.internal.LogDebugCheck;
import bitronix.tm.utils.Decoder;
import bitronix.tm.utils.MonotonicClock;
import bitronix.tm.utils.Uid;

import jakarta.transaction.Status;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;

/**
 * Simple implementation of a journal that writes on a two-files disk log.
 * 

Files are pre-allocated in size, never grow and when the first one is full, dangling records are copied to the * second file and logging starts again on the latter.

*

This implementation is not highly efficient but quite robust and simple. It is based on one of the implementations * proposed by Mike Spille.

*

Configurable properties are all starting with bitronix.tm.journal.disk.

* * @author Ludovic Orban * @author Brett Wooldridge * @see bitronix.tm.Configuration * @see XA Exposed, Part III: The Implementor's Notebook */ public class DiskJournal implements Journal, MigratableJournal, ReadableJournal { private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(DiskJournal.class.toString()); /** * The active log appender. This is exactly the same reference as tla1 or tla2 depending on which one is * currently active */ private final AtomicReference activeTla; private final Lock conservativeJournalingLock = new ReentrantLock(); private final ReadWriteLock swapForceLock = new ReentrantReadWriteLock(true); private final Object positionLock = new Object(); private final AtomicBoolean needsForce; private final Configuration configuration; /** * The transaction log appender writing on the 1st file */ private TransactionLogAppender tla1; /** * The transaction log appender writing on the 2nd file */ private TransactionLogAppender tla2; /** * Create an uninitialized disk journal. You must call open() prior you can use it. */ public DiskJournal() { configuration = TransactionManagerServices.getConfiguration(); needsForce = new AtomicBoolean(); activeTla = new AtomicReference<>(); } /** * Log a new transaction status to journal. Note that the DiskJournal will not check the flow of the transaction. * If you call this method with erroneous data, it will be added to the journal anyway. * * @param status * transaction status to log. See {@link jakarta.transaction.Status} constants. * @param gtrid * raw GTRID of the transaction. * @param uniqueNames * unique names of the {@link bitronix.tm.resource.common.ResourceBean}s participating in * this transaction. * * @throws java.io.IOException * in case of disk IO failure or if the disk journal is not open. */ @Override public void log(int status, Uid gtrid, Set uniqueNames) throws IOException { if (activeTla.get() == null) { throw new IOException("cannot write log, disk logger is not open"); } if (configuration.isFilterLogStatus() && (status != Status.STATUS_COMMITTING && status != Status.STATUS_COMMITTED && status != Status.STATUS_UNKNOWN)) { if (LogDebugCheck.isDebugEnabled()) { log.finer("filtered out write to log for status " + Decoder.decodeStatus(status)); } return; } TransactionLogRecord tlog = new TransactionLogRecord(status, gtrid, uniqueNames); try { if (configuration.isConservativeJournaling()) { conservativeJournalingLock.lock(); } synchronized (positionLock) { boolean rollover = activeTla.get() .setPositionAndAdvance(tlog); if (rollover) { // time to swap log files swapForceLock.writeLock() .lock(); try { swapJournalFiles(); activeTla.get() .setPositionAndAdvance(tlog); } finally { swapForceLock.writeLock() .unlock(); } } // this read lock MUST be acquired under positionLock swapForceLock.readLock() .lock(); } try { activeTla.get() .writeLog(tlog); needsForce.set(true); } finally { swapForceLock.readLock() .unlock(); } } finally { if (configuration.isConservativeJournaling()) { conservativeJournalingLock.unlock(); } } } /** *

Swap the active and the passive journal files so that the active one becomes passive and the passive one * becomes active.

* List of actions taken by this method: *
    *
  • ensure the all data has been forced to the active log file.
  • *
  • copy dangling COMMITTING records to the passive log file.
  • *
  • update header timestamp of passive log file (makes it become active).
  • *
  • do a force on passive log file. It is now the active file.
  • *
  • switch references of active/passive files.
  • *
* * @throws java.io.IOException * in case of disk IO failure. */ private synchronized void swapJournalFiles() throws IOException { if (LogDebugCheck.isDebugEnabled()) { log.finer("swapping journal log file to " + getPassiveTransactionLogAppender()); } //step 1 activeTla.get() .force(); //step 2 TransactionLogAppender passiveTla = getPassiveTransactionLogAppender(); passiveTla.rewind(); List danglingLogs = activeTla.get() .getDanglingLogs(); for (TransactionLogRecord tlog : danglingLogs) { boolean rolloverError = passiveTla.setPositionAndAdvance(tlog); if (rolloverError) { throw new IOException("moving in-flight transactions the rollover log file would have resulted in an overflow of that file"); } passiveTla.writeLog(tlog); } if (LogDebugCheck.isDebugEnabled()) { log.finer(danglingLogs.size() + " dangling record(s) copied to passive log file"); } activeTla.get() .clearDanglingLogs(); //step 3 passiveTla.setTimestamp(MonotonicClock.currentTimeMillis()); //step 4 passiveTla.force(); //step 5 activeTla.set(passiveTla); if (LogDebugCheck.isDebugEnabled()) { log.finer("journal log files swapped"); } } /** * @return the TransactionFileAppender of the passive journal file. */ private synchronized TransactionLogAppender getPassiveTransactionLogAppender() { return (tla1 == activeTla.get() ? tla2 : tla1); } /** * Open the disk journal. Files are checked for integrity and DiskJournal will refuse to open corrupted log files. * If files are not present on disk, this method will create and pre-allocate them. * * @throws java.io.IOException * in case of disk IO failure. */ @Override public synchronized void open() throws IOException { if (activeTla.get() != null) { log.warning("disk journal already open"); return; } File file1 = new File(configuration.getLogPart1Filename()); File file2 = new File(configuration.getLogPart2Filename()); if (!file1.exists() && !file2.exists()) { log.finer("creation of log files"); createLogfile(file2, configuration.getMaxLogSizeInMb()); // make the clock run a little before creating the 2nd log file to ensure the timestamp headers are not the same long before = MonotonicClock.currentTimeMillis(); while (MonotonicClock.currentTimeMillis() < before + 100L) { try { wait(100); } catch (InterruptedException ex) { /* ignore */ } } createLogfile(file1, configuration.getMaxLogSizeInMb()); } if (file1.length() != file2.length()) { if (!configuration.isSkipCorruptedLogs()) { throw new IOException("transaction log files are not of the same length, assuming they're corrupt"); } log.severe("transaction log files are not of the same length: corrupted files?"); } long maxFileLength = Math.max(file1.length(), file2.length()); if (LogDebugCheck.isDebugEnabled()) { log.finer("disk journal files max length: " + maxFileLength); } tla1 = new TransactionLogAppender(file1, maxFileLength); tla2 = new TransactionLogAppender(file2, maxFileLength); byte cleanStatus = pickActiveJournalFile(tla1, tla2); if (cleanStatus != TransactionLogHeader.CLEAN_LOG_STATE) { log.warning("active log file is unclean, did you call BitronixTransactionManager.shutdown() at the end of the last run?"); } if (LogDebugCheck.isDebugEnabled()) { log.finer("disk journal opened"); } } /** * Create a fresh log file on disk. If the specified file already exists it will be deleted then recreated. * * @param logfile * the file to create * @param maxLogSizeInMb * the file size in megabytes to preallocate * * @throws java.io.IOException * in case of disk IO failure. */ private static void createLogfile(File logfile, int maxLogSizeInMb) throws IOException { if (logfile.isDirectory()) { throw new IOException("log file is referring to a directory: " + logfile.getAbsolutePath()); } if (logfile.exists()) { boolean deleted = logfile.delete(); if (!deleted) { throw new IOException("log file exists but cannot be overwritten: " + logfile.getAbsolutePath()); } } if (logfile.getParentFile() != null) { logfile.getParentFile() .mkdirs(); } try (RandomAccessFile raf = new RandomAccessFile(logfile, "rw")) { raf.seek(TransactionLogHeader.FORMAT_ID_HEADER); raf.writeInt(BitronixXid.FORMAT_ID); raf.writeLong(MonotonicClock.currentTimeMillis()); raf.writeByte(TransactionLogHeader.CLEAN_LOG_STATE); raf.writeLong(TransactionLogHeader.HEADER_LENGTH); byte[] buffer = new byte[4096]; int length = (maxLogSizeInMb * 1024 * 1024) / 4096; for (int i = 0; i < length; i++) { raf.write(buffer); } } } /** * Initialize the activeTla member variable with the TransactionLogAppender object having the latest timestamp * header. * * @param tla1 * the first of the two candidate active TransactionLogAppenders * @param tla2 * the second of the two candidate active TransactionLogAppenders * * @return the state of the designated active TransactionLogAppender as returned by TransactionLogHeader.getState() * * @throws java.io.IOException * in case of disk IO failure. * @see TransactionLogHeader */ private synchronized byte pickActiveJournalFile(TransactionLogAppender tla1, TransactionLogAppender tla2) throws IOException { if (tla1.getTimestamp() > tla2.getTimestamp()) { activeTla.set(tla1); if (LogDebugCheck.isDebugEnabled()) { log.finer("logging to file 1: " + activeTla); } } else { activeTla.set(tla2); if (LogDebugCheck.isDebugEnabled()) { log.finer("logging to file 2: " + activeTla); } } byte cleanState = activeTla.get() .getState(); activeTla.get() .setState(TransactionLogHeader.UNCLEAN_LOG_STATE); if (LogDebugCheck.isDebugEnabled()) { log.finer("log file activated, forcing file state to disk"); } activeTla.get() .force(); return cleanState; } /** * Close the disk journal and the underlying files. * * @throws java.io.IOException * in case of disk IO failure. */ @Override public synchronized void close() throws IOException { if (activeTla.get() == null) { return; } try { tla1.close(); } catch (IOException ex) { log.log(Level.SEVERE, "cannot close " + tla1, ex); } tla1 = null; try { tla2.close(); } catch (IOException ex) { log.log(Level.SEVERE, "cannot close " + tla2, ex); } tla2 = null; activeTla.set(null); if (LogDebugCheck.isDebugEnabled()) { log.finer("disk journal closed"); } } /** * Force active log file to synchronize with the underlying disk device. * * @throws java.io.IOException * in case of disk IO failure or if the disk journal is not open. */ @Override public void force() throws IOException { if (activeTla.get() == null) { throw new IOException("cannot force log writing, disk logger is not open"); } if (needsForce.get() && configuration.isForcedWriteEnabled()) { swapForceLock.writeLock() .lock(); try { activeTla.get() .force(); needsForce.set(false); } finally { swapForceLock.writeLock() .unlock(); } } } /* * Internal impl. */ /** * Collect all dangling records of the active log file. * * @return a Map using Uid objects GTRID as key and {@link TransactionLogRecord} as value * * @throws java.io.IOException * in case of disk IO failure or if the disk journal is not open. */ @Override public Map collectDanglingRecords() throws IOException { if (activeTla.get() == null) { throw new IOException("cannot collect dangling records, disk logger is not open"); } return collectDanglingRecords(activeTla.get()); } /** * {@inheritDoc} */ @Override public void migrateTo(Journal other) throws IOException { if (other == this) { throw new IllegalArgumentException("cannot migrate a journal to itself (this == otherJournal)"); } if (other == null) { throw new IllegalArgumentException("the migration target journal cannot be null"); } for (Object record : collectDanglingRecords().values()) { JournalRecord jr = (JournalRecord) record; other.log(jr.getStatus(), jr.getGtrid(), jr.getUniqueNames()); } } /** * Create a Map of TransactionLogRecord with COMMITTING status objects using the GTRID byte[] as key that have * no corresponding COMMITTED record * * @param tla * the TransactionLogAppender to scan * * @return a Map using Uid objects GTRID as key and {@link TransactionLogRecord} as value * * @throws java.io.IOException * in case of disk IO failure. */ private static Map collectDanglingRecords(TransactionLogAppender tla) throws IOException { Map danglingRecords = new HashMap<>(64); TransactionLogCursor tlc = tla.getCursor(); try { int committing = 0; Integer committed = 0; while (true) { TransactionLogRecord tlog; try { tlog = tlc.readLog(); } catch (CorruptedTransactionLogException ex) { if (TransactionManagerServices.getConfiguration() .isSkipCorruptedLogs()) { log.log(Level.SEVERE, "skipping corrupted log", ex); continue; } throw ex; } if (tlog == null) { break; } int status = tlog.getStatus(); if (status == Status.STATUS_COMMITTING) { danglingRecords.put(tlog.getGtrid(), tlog); committing++; } // COMMITTED is when there was no problem in the transaction // UNKNOWN is when a 2PC transaction heuristically terminated // ROLLEDBACK is when a 1PC transaction rolled back during commit if (status == Status.STATUS_COMMITTED || status == Status.STATUS_UNKNOWN || status == Status.STATUS_ROLLEDBACK) { committed = processTransaction(danglingRecords, tlog, committed); } } if (LogDebugCheck.isDebugEnabled()) { log.finer("collected dangling records of " + tla + ", committing: " + committing + ", committed: " + committed + ", delta: " + danglingRecords.size()); } } finally { tlc.close(); } return danglingRecords; } /** * Method processTransaction ... * * @param danglingRecords * of type Map * @param tlog * of type TransactionLogRecord * @param committed * of type int * * @return int */ private static int processTransaction(Map danglingRecords, TransactionLogRecord tlog, int committed) { JournalRecord rec = danglingRecords.get(tlog.getGtrid()); if (rec != null) { Set recUniqueNames = new HashSet<>(rec.getUniqueNames()); recUniqueNames.removeAll(tlog.getUniqueNames()); if (recUniqueNames.isEmpty()) { danglingRecords.remove(tlog.getGtrid()); committed++; } else { danglingRecords.put(tlog.getGtrid(), new TransactionLogRecord(rec.getStatus(), rec.getGtrid(), recUniqueNames)); } } return committed; } /** * {@inheritDoc} */ @Override public synchronized void unsafeReadRecordsInto(Collection target, boolean includeInvalid) throws IOException { if (activeTla.get() == null) { throw new IOException("cannot read records, disk logger is not open"); } for (Iterator i = iterateRecords(activeTla.get(), includeInvalid); i != null && i.hasNext(); ) { target.add(i.next()); } } /** * Implements a low level iterator over all entries contained in the active TX log. * * @param tla * the TransactionLogAppender to scan * @param skipCrcCheck * sets whether CRC checks are applied or not. * * @return an iterator over all contained log records. * * @throws java.io.IOException * in case of the initial disk IO failed (subsequent errors are unchecked exceptions). */ private static Iterator iterateRecords(TransactionLogAppender tla, boolean skipCrcCheck) throws IOException { TransactionLogCursor tlc = tla.getCursor(); Iterator it = new TransactionLogIterator(tlc, skipCrcCheck); try { if (it.hasNext()) { return it; } else { return null; } } catch (RuntimeException ex) { if (ex.getCause() instanceof IOException) { throw (IOException) ex.getCause(); } throw ex; } } /** * Shutdown the service and free all held resources. */ @Override public void shutdown() { try { close(); } catch (IOException ex) { log.log(Level.SEVERE, "error shutting down disk journal. Transaction log integrity could be compromised!", ex); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy