/*
* 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.exception.MuleRuntimeException;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages a transaction journal file.
*
* @param <T> type of the transaction identifier
* @param <K> type of the journal entry
*/
class TransactionJournalFile<T, K extends JournalEntry<T>> {
/**
* Defines the minimum number of entries in the log that are required to clear the log file once there are no more transactions
* pending.
*/
private static final int MINIMUM_ENTRIES_TO_CLEAR_FILE = 10000;
protected transient Logger logger = LoggerFactory.getLogger(getClass());
private final File journalFile;
private final JournalEntrySerializer<T, K> journalEntrySerializer;
private final Long clearFileMinimumSizeInBytes;
private Multimap<T, K> entries = LinkedHashMultimap.create();
private DataOutputStream logFileOutputStream;
private int journalOperations = 0;
/**
* @param journalFile journal file to use. Will be created if it doesn't exists. If exists then transaction entries will get
* loaded from it.
* @param journalEntrySerializer serializer for {@link JournalEntry}
* @param transactionCompletePredicate a callback to determine if a transaction is complete.
*/
public TransactionJournalFile(File journalFile, JournalEntrySerializer journalEntrySerializer,
TransactionCompletePredicate transactionCompletePredicate, Long clearFileMinimumSizeInBytes) {
this.journalFile = journalFile;
this.journalEntrySerializer = journalEntrySerializer;
this.clearFileMinimumSizeInBytes = clearFileMinimumSizeInBytes;
if (journalFile.exists()) {
loadAllEntries(transactionCompletePredicate);
}
createLogOutputStream();
}
/**
* Adds a journal entry for an operation done over a transactional resource
*
* @param journalEntry operation details
*/
public synchronized void logOperation(K journalEntry) {
entries.put(journalEntry.getTxId(), journalEntry);
journalEntrySerializer.serialize(journalEntry, logFileOutputStream);
journalOperations++;
}
/**
* Removes all journal entries for a particular transactions.
*
* If there are no transaction entries left then the journal file is emptied.
*
* @param txId transaction identifier
*/
public synchronized void clearEntriesForTransaction(T txId) {
doClearEntriesForTransaction(txId);
clearFileIfNeeded();
}
protected void doClearEntriesForTransaction(T txId) {
Collection<K> entries = this.entries.removeAll(txId);
if (logger.isDebugEnabled()) {
logger.debug("Evicted from tx log file " + entries.size() + " entries from txid " + txId);
}
}
protected void clearFileIfNeeded() {
if (this.entries.isEmpty()) {
if (clearFileMinimumSizeInBytes != null) {
if (fileLength() > clearFileMinimumSizeInBytes) {
clear();
journalOperations = 0;
}
} else if (journalOperations > MINIMUM_ENTRIES_TO_CLEAR_FILE) {
clear();
journalOperations = 0;
}
}
}
/**
* Free all resources held.
*
* Will not remove the transaction context.
*/
public synchronized void close() {
try {
logFileOutputStream.close();
} catch (IOException e) {
logger.warn(e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("Error closing transaction journal file", e);
}
}
}
/**
* @param txId transaction identifier
* @return a copy collection of the journal entries for txId
*/
public Collection<K> getLogEntries(T txId) {
final Collection<K> entries = this.entries.asMap().get(txId);
if (entries == null) {
return Collections.emptyList();
}
synchronized (entries) {
return Collections.unmodifiableCollection(new ArrayList<>(entries));
}
}
/**
* @return all journal entries exactly as stored in the journal. No modifications should be done to such collections and the
* journal file should not be access concurrently for other purposes while working with the collection.
*/
public synchronized Multimap<T, K> getAllLogEntries() {
return entries;
}
/**
* Remove all the entries from the transaction journal and cleans the transaction journal fle.
*/
public synchronized void clear() {
close();
entries.clear();
FileUtils.deleteQuietly(journalFile);
createLogOutputStream();
}
private void createLogOutputStream() {
if (!journalFile.exists()) {
try {
journalFile.createNewFile();
} catch (IOException e) {
throw new MuleRuntimeException(e);
}
}
try {
this.logFileOutputStream = new DataOutputStream(new FileOutputStream(journalFile, true));
} catch (FileNotFoundException e) {
throw new MuleRuntimeException(e);
}
}
/**
* This will NOT load transactions that are already complete, according to the given {@code transactionCompletePredicate}.
*
* @param transactionCompletePredicate a callback to determine if a transaction is complete.
*/
private void loadAllEntries(TransactionCompletePredicate transactionCompletePredicate) {
if (!journalFile.exists()) {
return;
}
DataInputStream dataInputStream = null;
try {
dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(journalFile)));
boolean logEntryCreationFailed = false;
while (!logEntryCreationFailed) {
try {
K journalEntry = journalEntrySerializer.deserialize(dataInputStream);
if (journalEntry != null) {
this.entries.put(journalEntry.getTxId(), journalEntry);
journalOperations++;
if (transactionCompletePredicate.isTransactionComplete(journalEntry)) {
journalOperations -= this.entries.get(journalEntry.getTxId()).size();
doClearEntriesForTransaction(journalEntry.getTxId());
}
} else {
logEntryCreationFailed = true;
}
} catch (EOFException e) {
logger.debug("Expected exception since there are no more log entries", e);
logEntryCreationFailed = true;
} catch (Exception e) {
logger.warn("Exception reading transaction content. This is normal if the mule server was shutdown due to a failure"
+ e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("Error reading transaction journal file", e);
}
logEntryCreationFailed = true;
}
}
clearFileIfNeeded();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} finally {
try {
if (dataInputStream != null) {
dataInputStream.close();
}
} catch (IOException e) {
logger.error("Error loading transaction journal file entries", e);
}
}
}
/**
* @return the number different transaction identifier held by the journal.
*/
public int size() {
return entries.size();
}
/**
* @param txId transaction identifier
* @return true if contains entries for that txId, false otherwise
*/
public boolean containsTx(T txId) {
return this.entries.containsKey(txId);
}
/**
* @return the file length in bytes
*/
public long fileLength() {
return journalFile.length();
}
}