//Prevayler(TM) - The Free-Software Prevalence Layer.
//Copyright (C) 2001-2004 Klaus Wuestefeld
//This library 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.
//Contributions: Carlos Villela.
package org.prevayler.implementation.journal;
import org.prevayler.foundation.Chunk;
import org.prevayler.foundation.DurableInputStream;
import org.prevayler.foundation.DurableOutputStream;
import org.prevayler.foundation.StopWatch;
import org.prevayler.foundation.monitor.Monitor;
import org.prevayler.implementation.PrevaylerDirectory;
import org.prevayler.implementation.TransactionGuide;
import org.prevayler.implementation.TransactionTimestamp;
import org.prevayler.implementation.publishing.TransactionSubscriber;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
/**
* A Journal that will write all transactions to .journal files.
*/
public class PersistentJournal implements Journal {
private final PrevaylerDirectory _directory;
private DurableOutputStream _outputJournal;
private final long _journalSizeThresholdInBytes;
private final long _journalAgeThresholdInMillis;
private StopWatch _journalAgeTimer;
private long _nextTransaction;
private boolean _nextTransactionInitialized = false;
private Monitor _monitor;
private final String _journalSuffix;
/**
* @param directory
* @param journalSizeThresholdInBytes
* Size of the current journal file beyond which it is closed and
* a new one started. Zero indicates no size threshold. This is
* useful journal backup purposes.
* @param journalAgeThresholdInMillis
* Age of the current journal file beyond which it is closed and
* a new one started. Zero indicates no age threshold. This is
* useful journal backup purposes.
*/
public PersistentJournal(PrevaylerDirectory directory,
long journalSizeThresholdInBytes, long journalAgeThresholdInMillis,
String journalSuffix, Monitor monitor) throws IOException {
this(directory, journalSizeThresholdInBytes,
journalAgeThresholdInMillis, journalSuffix);
_monitor = monitor;
}
public PersistentJournal(PrevaylerDirectory directory,
long journalSizeThresholdInBytes, long journalAgeThresholdInMillis,
String journalSuffix) throws IOException {
PrevaylerDirectory.checkValidJournalSuffix(journalSuffix);
_directory = directory;
_directory.produceDirectory();
_journalSizeThresholdInBytes = journalSizeThresholdInBytes;
_journalAgeThresholdInMillis = journalAgeThresholdInMillis;
_journalSuffix = journalSuffix;
}
public void append(TransactionGuide guide) {
if (!_nextTransactionInitialized)
throw new IllegalStateException(
"Journal.update() has to be called at least once before Journal.append().");
DurableOutputStream myOutputJournal;
DurableOutputStream outputJournalToClose = null;
guide.startTurn();
try {
guide.checkSystemVersion(_nextTransaction);
if (!isOutputJournalStillValid()) {
outputJournalToClose = _outputJournal;
_outputJournal = createOutputJournal(_nextTransaction);
_journalAgeTimer = StopWatch.start();
}
_nextTransaction++;
myOutputJournal = _outputJournal;
} finally {
guide.endTurn();
}
try {
myOutputJournal.sync(guide);
} catch (IOException iox) {
handle(iox, _outputJournal.file(), "writing to");
}
guide.startTurn();
try {
try {
if (outputJournalToClose != null)
outputJournalToClose.close();
} catch (IOException iox) {
handle(iox, outputJournalToClose.file(), "closing");
}
} finally {
guide.endTurn();
}
}
private boolean isOutputJournalStillValid() {
return _outputJournal != null && !isOutputJournalTooBig()
&& !isOutputJournalTooOld();
}
private boolean isOutputJournalTooOld() {
return _journalAgeThresholdInMillis != 0
&& _journalAgeTimer.millisEllapsed() >= _journalAgeThresholdInMillis;
}
private boolean isOutputJournalTooBig() {
return _journalSizeThresholdInBytes != 0
&& _outputJournal.file().length() >= _journalSizeThresholdInBytes;
}
private DurableOutputStream createOutputJournal(long transactionNumber) {
File file = _directory.journalFile(transactionNumber, _journalSuffix);
try {
return new DurableOutputStream(file);
} catch (IOException iox) {
handle(iox, file, "creating");
return null;
}
}
/**
* IMPORTANT: This method cannot be called while the log() method is being
* called in another thread. If there are no journal files in the directory
* (when a snapshot is taken and all journal files are manually deleted, for
* example), the initialTransaction parameter in the first call to this
* method will define what the next transaction number will be. We have to
* find clearer/simpler semantics.
*/
public void update(TransactionSubscriber subscriber,
long initialTransactionWanted) throws IOException,
ClassNotFoundException {
File initialJournal = _directory
.findInitialJournalFile(initialTransactionWanted);
if (initialJournal == null) {
initializeNextTransaction(initialTransactionWanted, 1);
return;
}
long nextTransaction = recoverPendingTransactions(subscriber,
initialTransactionWanted, initialJournal);
initializeNextTransaction(initialTransactionWanted, nextTransaction);
}
private void initializeNextTransaction(long initialTransactionWanted,
long nextTransaction) throws IOException {
if (_nextTransactionInitialized) {
if (_nextTransaction < initialTransactionWanted)
throw new IOException(
"The transaction log has not yet reached transaction "
+ initialTransactionWanted
+ ". The last logged transaction was "
+ (_nextTransaction - 1) + ".");
if (nextTransaction < _nextTransaction)
throw new IOException(
"Unable to find journal file containing transaction "
+ nextTransaction
+ ". Might have been manually deleted.");
if (nextTransaction > _nextTransaction)
throw new IllegalStateException();
return;
}
_nextTransactionInitialized = true;
_nextTransaction = initialTransactionWanted > nextTransaction ? initialTransactionWanted
: nextTransaction;
}
private DurableInputStream getInputStream(File journal) throws IOException{
if (true)
return new DurableInputStream(journal, _monitor);
return new DurableInputStream(journal);
}
private long recoverPendingTransactions(TransactionSubscriber subscriber,
long initialTransaction, File initialJournal) throws IOException {
long recoveringTransaction = PrevaylerDirectory
.journalVersion(initialJournal);
File journal = initialJournal;
DurableInputStream input = getInputStream(journal);
while (true) {
try {
Chunk chunk = input.readChunk();
if (recoveringTransaction >= initialTransaction) {
if (!journal.getName().endsWith(_journalSuffix)) {
throw new IOException(
"There are transactions needing to be recovered from "
+ journal + ", but only "
+ _journalSuffix
+ " files are supported");
}
TransactionTimestamp entry = TransactionTimestamp
.fromChunk(chunk);
if (entry.systemVersion() != recoveringTransaction) {
throw new IOException("Expected "
+ recoveringTransaction + " but was "
+ entry.systemVersion());
}
subscriber.receive(entry);
}
recoveringTransaction++;
} catch (EOFException eof) {
File nextFile = _directory.journalFile(recoveringTransaction,
_journalSuffix);
if (journal.equals(nextFile))
PrevaylerDirectory.renameUnusedFile(journal); // The first
// transaction
// in this
// log file
// is
// incomplete.
// We need
// to reuse
// this file
// name.
journal = nextFile;
if (!journal.exists())
break;
input=getInputStream(journal);
}
}
return recoveringTransaction;
}
protected void handle(IOException iox, File journal, String action) {
String message = "All transaction processing is now blocked. An IOException was thrown while "
+ action + " a .journal file.";
_monitor.notify(this.getClass(), message, journal, iox);
hang();
}
static private void hang() {
while (true) {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
}
}
public void close() throws IOException {
if (_outputJournal != null)
_outputJournal.close();
}
public long nextTransaction() {
if (!_nextTransactionInitialized)
throw new IllegalStateException(
"update() must be called at least once");
return _nextTransaction;
}
}