/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.core.util.journal;
import org.mule.runtime.api.util.Preconditions;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Keeps track of transactional operation made over a transactional resource.
*
* Uses two files for keeping track of the transactions and empties a file once all the entries in such file are resolved.
*
* Keeps a memory cache of the log entries for performance reasons. Once a transaction finishes the client of this class must
* manually remove the entries related to such transaction to clear the cache.
*/
public class TransactionJournal<T, K extends JournalEntry<T>> {
public static final String TX1_LOG_FILE_NAME = "tx1.log";
public static final String TX2_LOG_FILE_NAME = "tx2.log";
private static final int MAXIMUM_LOG_FILE_ENTRIES = 50000;
private static final int ONE_MEGABYTE_IN_BYTES = 1024 * 1024;
private transient Logger logger = LoggerFactory.getLogger(getClass());
private final TransactionCompletePredicate transactionCompletePredicate;
/**
* Log file in which we are currently writing new entries.
*/
private TransactionJournalFile<T, K> currentLogFile;
/**
* Log file which has old entries and will be cleared as soon as all the transaction in it are resolved.
*/
private TransactionJournalFile<T, K> notCurrentLogFile;
/**
* Maximum transaction log file size in bytes.
*/
private Long maximumFileSizeInBytes;
/**
* Minimum transaction log file size in bytes to consider it ready for clear content.
*/
private Long clearFileMinimumSizeInBytes;
/**
* @param logFilesDirectory directory used to store the journal files.
*/
public TransactionJournal(String logFilesDirectory, TransactionCompletePredicate transactionCompletePredicate,
JournalEntrySerializer journalEntrySerializer, Integer maximumFileSizeInMegabytes) {
File logFileDirectory = new File(logFilesDirectory);
if (!logFileDirectory.exists()) {
Preconditions.checkState(logFileDirectory.mkdirs(),
"Could not create directory for queue transaction logger " + logFileDirectory);
}
calculateJournalFileSize(maximumFileSizeInMegabytes);
File logFile1 = new File(logFileDirectory, TX1_LOG_FILE_NAME);
File logFile2 = new File(logFileDirectory, TX2_LOG_FILE_NAME);
logger.info(String.format("Using files for tx logs %s and %s", logFile1.getAbsolutePath(), logFile2.getAbsolutePath()));
this.currentLogFile =
new TransactionJournalFile(logFile1, journalEntrySerializer, transactionCompletePredicate, clearFileMinimumSizeInBytes);
this.notCurrentLogFile =
new TransactionJournalFile(logFile2, journalEntrySerializer, transactionCompletePredicate, clearFileMinimumSizeInBytes);
this.transactionCompletePredicate = transactionCompletePredicate;
}
private void calculateJournalFileSize(Integer maximumFileSizeInMegabytes) {
if (maximumFileSizeInMegabytes != null) {
this.maximumFileSizeInBytes = ((long) maximumFileSizeInMegabytes * ONE_MEGABYTE_IN_BYTES) / 2;
this.clearFileMinimumSizeInBytes = this.maximumFileSizeInBytes / 2;
}
}
/**
* Log an update operation over a transactional resource
*
* @param journalEntry journal entry with the update operation details
*/
public synchronized void logUpdateOperation(JournalEntry<T> journalEntry) {
TransactionJournalFile logFile = determineLogFile(journalEntry.getTxId());
logFile.logOperation(journalEntry);
}
/**
* Logs a checkpoint operation over the transactions.
*
* Most likely this is an operation such as COMMIT, ROLLBACK or PREPARE of a TX.
*
* @param journalEntry journal entry with the checkpoint operation details
*/
public synchronized void logCheckpointOperation(JournalEntry<T> journalEntry) {
TransactionJournalFile logFile = determineLogFile(journalEntry.getTxId());
logFile.logOperation(journalEntry);
if (transactionCompletePredicate.isTransactionComplete(journalEntry)) {
logFile.clearEntriesForTransaction(journalEntry.getTxId());
}
}
/**
* @param txId transaction identifier
* @return all the transaction entries for a certain transaction identifier
*/
public Collection<K> getLogEntriesForTx(T txId) {
TransactionJournalFile logFile = determineLogFileWithoutModifyingCurrent(txId);
if (logFile == null || !logFile.containsTx(txId)) {
return Collections.emptyList();
}
return logFile.getLogEntries(txId);
}
/**
* @return all the transactional entries from the journal
*/
public synchronized Multimap<T, K> getAllLogEntries() {
LinkedHashMultimap<T, K> logEntries = LinkedHashMultimap.create();
logEntries.putAll(currentLogFile.getAllLogEntries());
logEntries.putAll(notCurrentLogFile.getAllLogEntries());
return logEntries;
}
/**
* Release the resources used by the transaction journal
*/
public synchronized void close() {
currentLogFile.close();
notCurrentLogFile.close();
}
/**
* Removes all the entries from the transactional journal
*/
public synchronized void clear() {
currentLogFile.clear();
notCurrentLogFile.clear();
}
private TransactionJournalFile determineLogFile(T txId) {
final TransactionJournalFile logFile = determineLogFileWithoutModifyingCurrent(txId);
if (logFile != null) {
return logFile;
}
// we keep this condition for backward compatibility.
if (maximumFileSizeInBytes == null) {
if (currentLogFile.size() > MAXIMUM_LOG_FILE_ENTRIES && notCurrentLogFile.size() == 0) {
debugLogFilesSwap();
changeCurrentLogFile();
}
} else {
if (currentLogFile.fileLength() > maximumFileSizeInBytes && notCurrentLogFile.size() == 0) {
debugLogFilesSwap();
changeCurrentLogFile();
}
}
return currentLogFile;
}
private void debugLogFilesSwap() {
if (logger.isDebugEnabled()) {
logger.debug("Changing files, current file size: " + currentLogFile.fileLength() + " other file size: "
+ notCurrentLogFile.fileLength());
}
}
private void changeCurrentLogFile() {
TransactionJournalFile aux = currentLogFile;
currentLogFile = notCurrentLogFile;
notCurrentLogFile = aux;
}
private TransactionJournalFile determineLogFileWithoutModifyingCurrent(T txId) {
if (currentLogFile.containsTx(txId)) {
return currentLogFile;
}
if (notCurrentLogFile.containsTx(txId)) {
return notCurrentLogFile;
}
return null;
}
}