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.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 final String _journalSuffix; 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 long recoverPendingTransactions( TransactionSubscriber subscriber, long initialTransaction, File initialJournal) throws IOException { long recoveringTransaction=PrevaylerDirectory.journalVersion(initialJournal); File journal=initialJournal; DurableInputStream input; input=this.hook76(journal,input); 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); journal=nextFile; if (!journal.exists()) break; this.hook77(journal,input); } } 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."; this.hook78(iox,journal,message); 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; } protected DurableInputStream hook76( File journal, DurableInputStream input) throws IOException { return input; } protected void hook77( File journal, DurableInputStream input) throws IOException { } protected void hook78( IOException iox, File journal, String message){ } }