/**
* Copyright (c) 2002-2013 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.kernel.impl.transaction.xaframework;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;
import org.neo4j.helpers.Exceptions;
import org.neo4j.kernel.impl.cache.LruCache;
import org.neo4j.kernel.impl.util.ArrayMap;
import org.neo4j.kernel.impl.util.BufferedFileChannel;
import org.neo4j.kernel.impl.util.FileUtils;
import org.neo4j.kernel.impl.util.StringLogger;
/**
* <CODE>XaLogicalLog</CODE> is a transaction and logical log combined. In
* this log information about the transaction (such as started, prepared and
* committed) will be written. All commands participating in the transaction
* will also be written to the log.
* <p>
* Normally you don't have to do anything with this log except open it after it
* has been instanciated (see {@link XaContainer}). The only method that may be
* of use when implementing a XA compatible resource is the
* {@link #getCurrentTxIdentifier}. Leave everything else be unless you know
* what you're doing.
* <p>
* When the log is opened it will be scaned for uncompleted transactions and
* those transactions will be re-created. When scan of log is complete all
* transactions that hasn't entered prepared state will be marked as done
* (implies rolledback) and dropped. All transactions that have been prepared
* will be held in memory until the transaction manager tells them to commit.
* Transaction that already started commit but didn't get flagged as done will
* be re-committed.
*/
public class XaLogicalLog
{
private final Logger log;
private static final char CLEAN = 'C';
private static final char LOG1 = '1';
private static final char LOG2 = '2';
private FileChannel fileChannel = null;
private final ByteBuffer sharedBuffer;
private LogBuffer writeBuffer = null;
private long previousLogLastCommittedTx = -1;
private long logVersion = 0;
private final ArrayMap<Integer,LogEntry.Start> xidIdentMap =
new ArrayMap<Integer,LogEntry.Start>( 4, false, true );
private final Map<Integer,XaTransaction> recoveredTxMap =
new HashMap<Integer,XaTransaction>();
private int nextIdentifier = 1;
private boolean scanIsComplete = false;
private boolean nonCleanShutdown = false;
private String fileName = null;
private final XaResourceManager xaRm;
private final XaCommandFactory cf;
private final XaTransactionFactory xaTf;
private char currentLog = CLEAN;
private boolean keepLogs = false;
private boolean autoRotate = true;
private long rotateAtSize = 25*1024*1024; // 25MB
private final String storeDir;
private final LogBufferFactory logBufferFactory;
private boolean doingRecovery;
private long lastRecoveredTx = -1;
private long recoveredTxCount;
private final StringLogger msgLog;
private final LruCache<Long, TxPosition> txStartPositionCache =
new LruCache<Long, TxPosition>( "Tx start position cache", 10000, null );
private final LruCache<Long /*log version*/, Long /*last committed tx*/> logHeaderCache =
new LruCache<Long, Long>( "Log header cache", 1000, null );
XaLogicalLog( String fileName, XaResourceManager xaRm, XaCommandFactory cf,
XaTransactionFactory xaTf, Map<Object,Object> config )
{
this.fileName = fileName;
this.xaRm = xaRm;
this.cf = cf;
this.xaTf = xaTf;
this.logBufferFactory = (LogBufferFactory) config.get( LogBufferFactory.class );
log = Logger.getLogger( this.getClass().getName() + File.separator + fileName );
sharedBuffer = ByteBuffer.allocateDirect( 9 + Xid.MAXGTRIDSIZE
+ Xid.MAXBQUALSIZE * 10 );
storeDir = (String) config.get( "store_dir" );
msgLog = StringLogger.getLogger( storeDir);
// We should turn keep-logs on if there are previous logs around,
// this so that e.g. temporary shell sessions or operations don't create
// holes in the log history, because it's just annoying.
keepLogs = hasPreviousLogs();
}
synchronized void open() throws IOException
{
String activeFileName = fileName + ".active";
if ( !new File( activeFileName ).exists() )
{
if ( new File( fileName ).exists() )
{
// old < b8 xaframework with no log rotation and we need to
// do recovery on it
open( fileName );
}
else
{
open( getLog1FileName() );
setActiveLog( LOG1 );
}
}
else
{
FileChannel fc = new RandomAccessFile( activeFileName ,
"rw" ).getChannel();
byte bytes[] = new byte[256];
ByteBuffer buf = ByteBuffer.wrap( bytes );
int read = fc.read( buf );
fc.close();
if ( read != 4 )
{
throw new IllegalStateException( "Read " + read +
" bytes from " + activeFileName + " but expected 4" );
}
buf.flip();
char c = buf.asCharBuffer().get();
if ( c == CLEAN )
{
// clean
String newLog = getLog1FileName();
File file = new File( newLog );
if ( file.exists() )
{
renameLogFileToRightVersion( getLog1FileName(), file.length() );
}
file = new File( getLog2FileName() );
if ( file.exists() )
{
renameLogFileToRightVersion( getLog2FileName(), file.length() );
}
open( newLog );
setActiveLog( LOG1 );
}
else if ( c == LOG1 )
{
String newLog = getLog1FileName();
if ( !new File( newLog ).exists() )
{
throw new IllegalStateException(
"Active marked as 1 but no " + newLog + " exist" );
}
File otherLog = new File( getLog2FileName() );
if ( otherLog.exists() )
{
fixDualLogFiles( getLog1FileName(), getLog2FileName() );
}
currentLog = LOG1;
open( newLog );
}
else if ( c == LOG2 )
{
String newLog = getLog2FileName();
if ( !new File( newLog ).exists() )
{
throw new IllegalStateException(
"Active marked as 2 but no " + newLog + " exist" );
}
File otherLog = new File( getLog1FileName() );
if ( otherLog.exists() )
{
fixDualLogFiles( getLog2FileName(), getLog1FileName() );
}
currentLog = LOG2;
open( newLog );
}
else
{
throw new IllegalStateException( "Unknown active log: " + c );
}
}
instantiateCorrectWriteBuffer();
}
private void instantiateCorrectWriteBuffer() throws IOException
{
writeBuffer = instantiateCorrectWriteBuffer( fileChannel );
}
private LogBuffer instantiateCorrectWriteBuffer( FileChannel channel ) throws IOException
{
return logBufferFactory.create( channel );
}
private void open( String fileToOpen ) throws IOException
{
fileChannel = new RandomAccessFile( fileToOpen, "rw" ).getChannel();
if ( fileChannel.size() != 0 )
{
nonCleanShutdown = true;
doingRecovery = true;
try
{
doInternalRecovery( fileToOpen );
}
finally
{
doingRecovery = false;
}
}
else
{
logVersion = xaTf.getCurrentVersion();
long lastTxId = xaTf.getLastCommittedTx();
LogIoUtils.writeLogHeader( sharedBuffer, logVersion, lastTxId );
previousLogLastCommittedTx = lastTxId;
logHeaderCache.put( logVersion, previousLogLastCommittedTx );
fileChannel.write( sharedBuffer );
scanIsComplete = true;
msgLog.logMessage( "Opened [" + fileToOpen + "] clean empty log, version=" + logVersion + ", lastTxId=" + lastTxId, true );
}
}
public boolean scanIsComplete()
{
return scanIsComplete;
}
private int getNextIdentifier()
{
nextIdentifier++;
if ( nextIdentifier < 0 )
{
nextIdentifier = 1;
}
return nextIdentifier;
}
// returns identifier for transaction
// [TX_START][xid[gid.length,bid.lengh,gid,bid]][identifier][format id]
public synchronized int start( Xid xid ) throws XAException
{
int xidIdent = getNextIdentifier();
try
{
long position = writeBuffer.getFileChannelPosition();
LogEntry.Start start = new LogEntry.Start( xid, xidIdent, position );
LogIoUtils.writeStart( writeBuffer, xidIdent, xid );
xidIdentMap.put( xidIdent, start );
}
catch ( IOException e )
{
throw Exceptions.withCause( new XAException( "Logical log couldn't start transaction: " + e ), e );
}
return xidIdent;
}
// [TX_PREPARE][identifier]
public synchronized void prepare( int identifier ) throws XAException
{
LogEntry.Start startEntry = xidIdentMap.get( identifier );
assert startEntry != null;
try
{
LogIoUtils.writePrepare( writeBuffer, identifier );
writeBuffer.writeOut();
}
catch ( IOException e )
{
throw Exceptions.withCause( new XAException( "Logical log unable to mark prepare [" + identifier + "] " ),
e );
}
}
// [TX_1P_COMMIT][identifier]
public synchronized void commitOnePhase( int identifier, long txId, int masterId )
throws XAException
{
LogEntry.Start startEntry = xidIdentMap.get( identifier );
assert startEntry != null;
assert txId != -1;
try
{
LogIoUtils.writeCommit( false, writeBuffer, identifier, txId, masterId );
writeBuffer.force();
cacheTxStartPosition( txId, masterId, startEntry );
}
catch ( IOException e )
{
throw Exceptions.withCause(
new XAException( "Logical log unable to mark 1P-commit [" + identifier + "] " ), e );
}
}
private synchronized void cacheTxStartPosition( long txId, int masterId,
LogEntry.Start startEntry )
{
cacheTxStartPosition( txId, masterId, startEntry, logVersion );
}
private synchronized TxPosition cacheTxStartPosition( long txId, int masterId,
LogEntry.Start startEntry, long logVersion )
{
if ( startEntry.getStartPosition() == -1 )
{
throw new RuntimeException( "StartEntry.position is " + startEntry.getStartPosition() );
}
TxPosition result = new TxPosition( logVersion, masterId, startEntry.getIdentifier(),
startEntry.getStartPosition() );
txStartPositionCache.put( txId, result );
return result;
}
// [DONE][identifier]
public synchronized void done( int identifier ) throws XAException
{
assert xidIdentMap.get( identifier ) != null;
try
{
LogIoUtils.writeDone( writeBuffer, identifier );
xidIdentMap.remove( identifier );
}
catch ( IOException e )
{
throw Exceptions.withCause( new XAException( "Logical log unable to mark as done [" + identifier + "] " ),
e );
}
}
// [DONE][identifier] called from XaResourceManager during internal recovery
synchronized void doneInternal( int identifier ) throws IOException
{
if ( writeBuffer != null )
{ // For 2PC
LogIoUtils.writeDone( writeBuffer, identifier );
}
else
{ // For 1PC
sharedBuffer.clear();
LogIoUtils.writeDone( sharedBuffer, identifier );
sharedBuffer.flip();
fileChannel.write( sharedBuffer );
}
xidIdentMap.remove( identifier );
// force to make sure done record is there if 2PC tx and global log
// marks tx as committed
// fileChannel.force( false );
}
// [TX_2P_COMMIT][identifier]
public synchronized void commitTwoPhase( int identifier, long txId, int masterId )
throws XAException
{
LogEntry.Start startEntry = xidIdentMap.get( identifier );
assert startEntry != null;
assert txId != -1;
try
{
LogIoUtils.writeCommit( true, writeBuffer, identifier, txId, masterId );
writeBuffer.force();
cacheTxStartPosition( txId, masterId, startEntry );
}
catch ( IOException e )
{
throw Exceptions.withCause( new XAException( "Logical log unable to mark 2PC [" + identifier + "] " ), e );
}
}
// [COMMAND][identifier][COMMAND_DATA]
public synchronized void writeCommand( XaCommand command, int identifier )
throws IOException
{
checkLogRotation();
assert xidIdentMap.get( identifier ) != null;
LogIoUtils.writeCommand( writeBuffer, identifier, command );
}
private void applyEntry( LogEntry entry ) throws IOException
{
if ( entry instanceof LogEntry.Start )
{
applyStartEntry( (LogEntry.Start) entry );
}
else if ( entry instanceof LogEntry.Prepare )
{
applyPrepareEntry( (LogEntry.Prepare ) entry );
}
else if ( entry instanceof LogEntry.Command )
{
applyCommandEntry( (LogEntry.Command ) entry );
}
else if ( entry instanceof LogEntry.OnePhaseCommit )
{
applyOnePhaseCommitEntry( (LogEntry.OnePhaseCommit ) entry );
}
else if ( entry instanceof LogEntry.TwoPhaseCommit )
{
applyTwoPhaseCommitEntry( (LogEntry.TwoPhaseCommit ) entry );
}
else if ( entry instanceof LogEntry.Done )
{
applyDoneEntry( (LogEntry.Done ) entry );
}
else
{
throw new RuntimeException( "Unrecognized log entry " + entry );
}
}
private void applyStartEntry( LogEntry.Start entry) throws IOException
{
int identifier = entry.getIdentifier();
if ( identifier >= nextIdentifier )
{
nextIdentifier = (identifier + 1);
}
// re-create the transaction
Xid xid = entry.getXid();
xidIdentMap.put( identifier, entry );
XaTransaction xaTx = xaTf.create( identifier );
xaTx.setRecovered();
recoveredTxMap.put( identifier, xaTx );
xaRm.injectStart( xid, xaTx );
// force to make sure done record is there if 2PC tx and global log
// marks tx as committed
// fileChannel.force( false );
}
private void applyPrepareEntry( LogEntry.Prepare prepareEntry ) throws IOException
{
// get the tx identifier
int identifier = prepareEntry.getIdentifier();
LogEntry.Start entry = xidIdentMap.get( identifier );
if ( entry == null )
{
throw new IOException( "Unknown xid for identifier " + identifier );
}
Xid xid = entry.getXid();
if ( xaRm.injectPrepare( xid ) )
{
// read only we can remove
xidIdentMap.remove( identifier );
recoveredTxMap.remove( identifier );
}
}
private void applyOnePhaseCommitEntry( LogEntry.OnePhaseCommit commit )
throws IOException
{
int identifier = commit.getIdentifier();
long txId = commit.getTxId();
LogEntry.Start startEntry = xidIdentMap.get( identifier );
if ( startEntry == null )
{
throw new IOException( "Unknown xid for identifier " + identifier );
}
Xid xid = startEntry.getXid();
try
{
XaTransaction xaTx = xaRm.getXaTransaction( xid );
xaTx.setCommitTxId( txId );
xaRm.injectOnePhaseCommit( xid );
registerRecoveredTransaction( txId );
}
catch ( XAException e )
{
throw new IOException( e );
}
}
private void registerRecoveredTransaction( long txId )
{
if ( doingRecovery )
{
lastRecoveredTx = txId;
recoveredTxCount++;
}
}
private void logRecoveryMessage( String string )
{
if ( doingRecovery )
{
msgLog.logMessage( string, true );
}
}
private void applyDoneEntry( LogEntry.Done done ) throws IOException
{
// get the tx identifier
int identifier = done.getIdentifier();
LogEntry.Start entry = xidIdentMap.get( identifier );
if ( entry == null )
{
throw new IOException( "Unknown xid for identifier " + identifier );
}
Xid xid = entry.getXid();
xaRm.pruneXid( xid );
xidIdentMap.remove( identifier );
recoveredTxMap.remove( identifier );
}
private void applyTwoPhaseCommitEntry( LogEntry.TwoPhaseCommit commit ) throws IOException
{
int identifier = commit.getIdentifier();
long txId = commit.getTxId();
LogEntry.Start startEntry = xidIdentMap.get( identifier );
if ( startEntry == null )
{
throw new IOException( "Unknown xid for identifier " + identifier );
}
Xid xid = startEntry.getXid();
if ( xid == null )
{
throw new IOException( "Xid null for identifier " + identifier );
}
try
{
XaTransaction xaTx = xaRm.getXaTransaction( xid );
xaTx.setCommitTxId( txId );
xaRm.injectTwoPhaseCommit( xid );
registerRecoveredTransaction( txId );
}
catch ( XAException e )
{
throw new IOException( e );
}
}
private void applyCommandEntry( LogEntry.Command entry ) throws IOException
{
int identifier = entry.getIdentifier();
XaCommand command = entry.getXaCommand();
if ( command == null )
{
throw new IOException( "Null command for identifier " + identifier );
}
command.setRecovered();
XaTransaction xaTx = recoveredTxMap.get( identifier );
xaTx.injectCommand( command );
}
private void checkLogRotation() throws IOException
{
if ( autoRotate &&
writeBuffer.getFileChannelPosition() >= rotateAtSize )
{
long currentPos = writeBuffer.getFileChannelPosition();
long firstStartEntry = getFirstStartEntry( currentPos );
// only rotate if no huge tx is running
if ( ( currentPos - firstStartEntry ) < rotateAtSize / 2 )
{
rotate();
}
}
}
private void fixDualLogFiles( String activeLog, String oldLog ) throws IOException
{
FileChannel activeLogChannel = new RandomAccessFile( activeLog, "r" ).getChannel();
long[] activeLogHeader = LogIoUtils.readLogHeader( ByteBuffer.allocate( 16 ), activeLogChannel, false );
activeLogChannel.close();
FileChannel oldLogChannel = new RandomAccessFile( oldLog, "r" ).getChannel();
long[] oldLogHeader = LogIoUtils.readLogHeader( ByteBuffer.allocate( 16 ), oldLogChannel, false );
oldLogChannel.close();
if ( oldLogHeader == null )
{
if ( !FileUtils.deleteFile( new File( oldLog ) ) )
{
throw new IOException( "Unable to delete " + oldLog );
}
}
else if ( activeLogHeader == null || activeLogHeader[0] > oldLogHeader[0] )
{
// we crashed in rotate after setActive but did not move the old log to the right name
// (and we do not know if keepLogs is true or not so play it safe by keeping it)
String newName = getFileName( oldLogHeader[0] );
if ( !FileUtils.renameFile( new File( oldLog ), new File( newName ) ) )
{
throw new IOException( "Unable to rename " + oldLog + " to " + newName );
}
}
else
{
assert activeLogHeader[0] < oldLogHeader[0];
// we crashed in rotate before setActive, do the rotate work again and delete old
if ( !FileUtils.deleteFile( new File( oldLog ) ) )
{
throw new IOException( "Unable to delete " + oldLog );
}
}
}
private void renameLogFileToRightVersion( String logFileName, long endPosition ) throws IOException
{
File file = new File( logFileName );
if ( !file.exists() )
{
throw new IOException( "Logical log[" + logFileName +
"] not found" );
}
FileChannel channel = new RandomAccessFile( logFileName, "rw" ).getChannel();
long[] header = LogIoUtils.readLogHeader( ByteBuffer.allocate( 16 ), channel, false );
try
{
FileUtils.truncateFile( channel, endPosition );
}
catch ( IOException e )
{
log.log( Level.WARNING,
"Failed to truncate log at correct size", e );
}
channel.close();
String newName;
if ( header == null )
{
// header was never written
newName = getFileName( -1 ) + "_empty_header_log_" + System.currentTimeMillis();
}
else
{
newName = getFileName( header[0] );
}
File newFile = new File( newName );
boolean renamed = FileUtils.renameFile( file, newFile );
if ( !renamed )
{
throw new IOException( "Failed to rename log to: " + newName );
}
}
private void deleteLogFile( String logFileName ) throws IOException
{
File file = new File( logFileName );
if ( !file.exists() )
{
throw new IOException( "Logical log[" + logFileName +
"] not found" );
}
boolean deleted = FileUtils.deleteFile( file );
if ( !deleted )
{
log.warning( "Unable to delete clean logical log[" + logFileName +
"]" );
}
}
private void releaseCurrentLogFile() throws IOException
{
if ( writeBuffer != null )
{
writeBuffer.force();
}
fileChannel.close();
fileChannel = null;
}
public synchronized void close() throws IOException
{
if ( fileChannel == null || !fileChannel.isOpen() )
{
log.fine( "Logical log: " + fileName + " already closed" );
return;
}
long endPosition = writeBuffer.getFileChannelPosition();
if ( xidIdentMap.size() > 0 )
{
log.info( "Close invoked with " + xidIdentMap.size() +
" running transaction(s). " );
writeBuffer.force();
fileChannel.close();
log.info( "Dirty log: " + fileName + "." + currentLog +
" now closed. Recovery will be started automatically next " +
"time it is opened." );
return;
}
releaseCurrentLogFile();
char logWas = currentLog;
if ( currentLog != CLEAN ) // again special case, see above
{
setActiveLog( CLEAN );
}
if ( !keepLogs )
{
if ( logWas == CLEAN )
{
// special case going from old xa version with no log rotation
// and we started with a recovery
deleteLogFile( fileName );
}
else
{
deleteLogFile( fileName + "." + logWas );
}
}
else
{
renameLogFileToRightVersion( fileName + "." + logWas, endPosition );
xaTf.getAndSetNewVersion();
}
msgLog.logMessage( "Closed log " + fileName, true );
}
private long[] readAndAssertLogHeader( ByteBuffer localBuffer,
ReadableByteChannel channel, long expectedVersion ) throws IOException
{
long[] header = LogIoUtils.readLogHeader( localBuffer, channel, true );
if ( header[0] != expectedVersion )
{
throw new IOException( "Wrong version in log. Expected " + expectedVersion +
", but got " + header[0] );
}
return header;
}
StringLogger getStringLogger()
{
return msgLog;
}
private void doInternalRecovery( String logFileName ) throws IOException
{
log.info( "Non clean shutdown detected on log [" + logFileName +
"]. Recovery started ..." );
msgLog.logMessage( "Non clean shutdown detected on log [" + logFileName +
"]. Recovery started ...", true );
// get log creation time
long[] header = readLogHeader( fileChannel, "Tried to do recovery on log with illegal format version" );
if ( header == null )
{
log.info( "Unable to read header information, "
+ "no records in logical log." );
msgLog.logMessage( "No log version found for " + logFileName, true );
fileChannel.close();
boolean success = FileUtils.renameFile( new File( logFileName ),
new File( logFileName + "_unknown_timestamp_" +
System.currentTimeMillis() + ".log" ) );
assert success;
fileChannel.close();
fileChannel = new RandomAccessFile( logFileName,
"rw" ).getChannel();
return;
}
logVersion = header[0];
long lastCommittedTx = header[1];
previousLogLastCommittedTx = lastCommittedTx;
logHeaderCache.put( logVersion, previousLogLastCommittedTx );
log.fine( "Logical log version: " + logVersion + " with committed tx[" +
lastCommittedTx + "]" );
msgLog.logMessage( "[" + logFileName + "] logVersion=" + logVersion +
" with committed tx=" + lastCommittedTx, true );
long logEntriesFound = 0;
long lastEntryPos = fileChannel.position();
fileChannel = new BufferedFileChannel( fileChannel );
LogEntry entry;
while ( (entry = readEntry()) != null )
{
applyEntry( entry );
logEntriesFound++;
lastEntryPos = fileChannel.position();
}
// make sure we overwrite any broken records
fileChannel = ((BufferedFileChannel)fileChannel).getSource();
fileChannel.position( lastEntryPos );
msgLog.logMessage( "[" + logFileName + "] entries found=" + logEntriesFound +
" lastEntryPos=" + lastEntryPos, true );
// zero out the slow way since windows don't support truncate very well
sharedBuffer.clear();
while ( sharedBuffer.hasRemaining() )
{
sharedBuffer.put( (byte)0 );
}
sharedBuffer.flip();
long endPosition = fileChannel.size();
do
{
long bytesLeft = fileChannel.size() - fileChannel.position();
if ( bytesLeft < sharedBuffer.capacity() )
{
sharedBuffer.limit( (int) bytesLeft );
}
fileChannel.write( sharedBuffer );
sharedBuffer.flip();
} while ( fileChannel.position() < endPosition );
fileChannel.position( lastEntryPos );
scanIsComplete = true;
String recoveryCompletedMessage = "Internal recovery completed, scanned " + logEntriesFound
+ " log entries. Recovered " + recoveredTxCount
+ " transactions. Last tx recovered: " + lastRecoveredTx;
log.fine( recoveryCompletedMessage );
msgLog.logMessage( recoveryCompletedMessage );
xaRm.checkXids();
if ( xidIdentMap.size() == 0 )
{
log.fine( "Recovery completed." );
msgLog.logMessage( "Recovery on log [" + logFileName + "] completed." );
}
else
{
log.fine( "[" + logFileName + "] Found " + xidIdentMap.size()
+ " prepared 2PC transactions." );
msgLog.logMessage( "Recovery on log [" + logFileName +
"] completed with " + xidIdentMap + " prepared transactions found." );
for ( LogEntry.Start startEntry : xidIdentMap.values() )
{
log.fine( "[" + logFileName + "] 2PC xid[" +
startEntry.getXid() + "]" );
}
}
recoveredTxMap.clear();
}
// for testing, do not use!
void reset()
{
xidIdentMap.clear();
recoveredTxMap.clear();
}
private LogEntry readEntry() throws IOException
{
long position = fileChannel.position();
LogEntry entry = LogIoUtils.readEntry( sharedBuffer, fileChannel, cf );
if ( entry instanceof LogEntry.Start )
{
((LogEntry.Start) entry).setStartPosition( position );
}
return entry;
}
private final ArrayMap<Thread,Integer> txIdentMap =
new ArrayMap<Thread,Integer>( 5, true, true );
void registerTxIdentifier( int identifier )
{
txIdentMap.put( Thread.currentThread(), identifier );
}
void unregisterTxIdentifier()
{
txIdentMap.remove( Thread.currentThread() );
}
/**
* If the current thread is committing a transaction the identifier of that
* {@link XaTransaction} can be obtained invoking this method.
*
* @return the identifier of the transaction committing or <CODE>-1</CODE>
* if current thread isn't committing any transaction
*/
public int getCurrentTxIdentifier()
{
Integer intValue = txIdentMap.get( Thread.currentThread() );
if ( intValue != null )
{
return intValue;
}
return -1;
}
public ReadableByteChannel getLogicalLog( long version ) throws IOException
{
return getLogicalLog( version, 0 );
}
public ReadableByteChannel getLogicalLog( long version, long position ) throws IOException
{
String name = getFileName( version );
if ( !new File( name ).exists() )
{
throw new IOException( "No such log version:" + version );
}
FileChannel channel = new RandomAccessFile( name, "r" ).getChannel();
channel.position( position );
return new BufferedFileChannel( channel );
}
private void extractPreparedTransactionFromLog( int identifier,
FileChannel logChannel, LogBuffer targetBuffer ) throws IOException
{
LogEntry.Start startEntry = xidIdentMap.get( identifier );
logChannel.position( startEntry.getStartPosition() );
LogEntry entry;
boolean found = false;
while ( (entry = LogIoUtils.readEntry( sharedBuffer, logChannel, cf )) != null )
{
// TODO For now just skip Prepare entries
if ( entry.getIdentifier() != identifier )
{
continue;
}
if ( entry instanceof LogEntry.Prepare )
{
break;
}
if ( entry instanceof LogEntry.Start || entry instanceof LogEntry.Command )
{
LogIoUtils.writeLogEntry( entry, targetBuffer );
found = true;
}
else
{
throw new RuntimeException( "Expected start or command entry but found: " + entry );
}
}
if ( !found )
{
throw new IOException( "Transaction for internal identifier[" + identifier +
"] not found in current log" );
}
}
// private void assertLogCanContainTx( long txId, long prevTxId ) throws IOException
// {
// if ( prevTxId >= txId )
// {
// throw new IOException( "Log says " + txId +
// " can not exist in this log (prev tx id=" + prevTxId + ")" );
// }
// }
public synchronized ReadableByteChannel getPreparedTransaction( int identifier )
throws IOException
{
FileChannel logChannel = (FileChannel) getLogicalLogOrMyselfPrepared( logVersion, 0 );
InMemoryLogBuffer localBuffer = new InMemoryLogBuffer();
extractPreparedTransactionFromLog( identifier, logChannel, localBuffer );
logChannel.close();
return localBuffer;
}
public synchronized void getPreparedTransaction( int identifier, LogBuffer targetBuffer )
throws IOException
{
FileChannel logChannel = (FileChannel) getLogicalLogOrMyselfPrepared( logVersion, 0 );
extractPreparedTransactionFromLog( identifier, logChannel, targetBuffer );
logChannel.close();
}
public class LogExtractor
{
/**
* If tx range is smaller than this threshold ask the position cache for the
* start position furthest back. Otherwise jump to right log and scan.
*/
private static final int CACHE_FIND_THRESHOLD = 100;
private final ByteBuffer localBuffer =
ByteBuffer.allocate( 9 + Xid.MAXGTRIDSIZE + Xid.MAXBQUALSIZE * 10 );
private ReadableByteChannel source;
private final LogEntryCollector collector;
private long version;
private LogEntry.Commit lastCommitEntry;
private LogEntry.Commit previousCommitEntry;
private final long startTxId;
private long nextExpectedTxId;
private int counter;
public LogExtractor( long startTxId, long endTxIdHint ) throws IOException
{
this.startTxId = startTxId;
this.nextExpectedTxId = startTxId;
long diff = endTxIdHint-startTxId + 1/*since they are inclusive*/;
if ( diff < CACHE_FIND_THRESHOLD )
{ // Find it from cache, we must check with all the requested transactions
// because the first committed transaction doesn't necessarily have its
// start record before the others.
TxPosition earliestPosition = getEarliestStartPosition( startTxId, endTxIdHint );
if ( earliestPosition != null )
{
this.version = earliestPosition.version;
this.source = getLogicalLogOrMyselfCommitted( version, earliestPosition.position );
}
}
if ( source == null )
{ // Find the start position by jumping to the right log and scan linearly.
// for consecutive transaction there's no scan needed, only the first one.
this.version = findLogContainingTxId( startTxId )[0];
this.source = getLogicalLogOrMyselfCommitted( version, 0 );
// To get to the right position to start reading entries from
readAndAssertLogHeader( localBuffer, source, version );
}
this.collector = new KnownTxIdCollector( startTxId );
}
private TxPosition getEarliestStartPosition( long startTxId, long endTxIdHint )
{
TxPosition earliest = null;
for ( long txId = startTxId; txId <= endTxIdHint; txId++ )
{
TxPosition position = txStartPositionCache.get( txId );
if ( position == null ) return null;
if ( earliest == null || position.earlierThan( earliest ) )
{
earliest = position;
}
}
return earliest;
}
/**
* @return the txId for the extracted tx. Or -1 if end-of-stream was reached.
* @throws RuntimeException if there was something unexpected with the stream.
*/
public long extractNext( LogBuffer target ) throws IOException
{
try
{
while ( this.version <= logVersion )
{
long result = collectNextFromCurrentSource( target );
if ( result != -1 )
{
// TODO Should be assertions?
if ( previousCommitEntry != null && result == previousCommitEntry.getTxId() ) continue;
if ( result != nextExpectedTxId )
{
throw new RuntimeException( "Expected txId " + nextExpectedTxId + ", but got " + result + " (starting from " + startTxId + ")" + " " + counter + ", " + previousCommitEntry + ", " + lastCommitEntry );
}
nextExpectedTxId++;
counter++;
return result;
}
if ( this.version < logVersion )
{
continueInNextLog();
}
else break;
}
return -1;
}
catch ( Exception e )
{
// Something is wrong with the cached tx start position for this (expected) tx,
// remove it from cache so that next request will have to bypass the cache
// txStartPositionCache.remove( nextExpectedTxId );
txStartPositionCache.clear();
msgLog.logMessage( fileName + ", " + e.getMessage() + ". Clearing tx start position cache" );
if ( e instanceof IOException ) throw (IOException) e;
else throw Exceptions.launderedException( e );
}
}
private void continueInNextLog() throws IOException
{
ensureSourceIsClosed();
this.source = getLogicalLogOrMyselfCommitted( ++version, 0 );
readAndAssertLogHeader( localBuffer, source, version ); // To get to the right position to start reading entries from
}
private long collectNextFromCurrentSource( LogBuffer target ) throws IOException
{
LogEntry entry = null;
while ( collector.hasInFutureQueue() || // if something in queue then don't read next entry
(entry = LogIoUtils.readEntry( localBuffer, source, cf )) != null )
{
LogEntry foundEntry = collector.collect( entry, target );
if ( foundEntry != null )
{ // It just wrote the transaction, w/o the done record though. Add it
previousCommitEntry = lastCommitEntry;
LogIoUtils.writeLogEntry( new LogEntry.Done( collector.getIdentifier() ), target );
lastCommitEntry = (LogEntry.Commit)foundEntry;
return lastCommitEntry.getTxId();
}
}
return -1;
}
public void close()
{
ensureSourceIsClosed();
}
@Override
protected void finalize() throws Throwable
{
ensureSourceIsClosed();
}
private void ensureSourceIsClosed()
{
try
{
if ( source != null )
{
source.close();
source = null;
}
}
catch ( IOException e )
{ // OK?
System.out.println( "Couldn't close logical after extracting transactions from it" );
e.printStackTrace();
}
}
}
public LogExtractor getLogExtractor( long startTxId, long endTxIdHint ) throws IOException
{
return new LogExtractor( startTxId, endTxIdHint );
}
public static final int MASTER_ID_REPRESENTING_NO_MASTER = -1;
public synchronized int getMasterIdForCommittedTransaction( long txId ) throws IOException
{
if ( txId == 1 )
{
return MASTER_ID_REPRESENTING_NO_MASTER;
}
TxPosition cache = txStartPositionCache.get( txId );
if ( cache != null )
{
return cache.masterId;
}
LogExtractor extractor = getLogExtractor( txId, txId );
try
{
if ( extractor.extractNext( NullLogBuffer.INSTANCE ) != -1 )
{
return extractor.lastCommitEntry.getMasterId();
}
throw new RuntimeException( "Unable to find commit entry for txId[" + txId + "]" );// in log[" + version + "]" );
}
finally
{
extractor.close();
}
}
public ReadableByteChannel getLogicalLogOrMyselfCommitted( long version, long position )
throws IOException
{
synchronized ( this )
{
if ( version == logVersion )
{
String currentLogName = getCurrentLogFileName();
FileChannel channel = new RandomAccessFile( currentLogName, "r" ).getChannel();
channel.position( position );
return new BufferedFileChannel( channel );
}
}
if ( version < logVersion )
{
return getLogicalLog( version, position );
}
else
{
throw new RuntimeException( "Version[" + version +
"] is higher then current log version[" + logVersion + "]" );
}
}
private ReadableByteChannel getLogicalLogOrMyselfPrepared( long version, long position )
throws IOException
{
if ( version < logVersion )
{
return getLogicalLog( version, position );
}
else if ( version == logVersion )
{
String currentLogName = getCurrentLogFileName();
FileChannel channel = new RandomAccessFile( currentLogName, "r" ).getChannel();
channel = new BufferedFileChannel( channel );
// Combined with the writeBuffer in cases where a DirectMappedLogBuffer
// is used, on Windows or when memory mapping is turned off.
// Otherwise the channel is returned directly.
channel = logBufferFactory.combine( channel, writeBuffer );
channel.position( position );
return channel;
}
else
{
throw new RuntimeException( "Version[" + version +
"] is higher then current log version[" + logVersion + "]" );
}
}
private String getCurrentLogFileName()
{
return currentLog == LOG1 ? getLog1FileName() : getLog2FileName();
}
private long[] findLogContainingTxId( long txId ) throws IOException
{
long version = logVersion;
long committedTx = previousLogLastCommittedTx;
while ( version >= 0 )
{
Long cachedLastTx = logHeaderCache.get( version );
if ( cachedLastTx != null )
{
committedTx = cachedLastTx;
}
else
{
ReadableByteChannel logChannel = getLogicalLogOrMyselfCommitted( version, 0 );
try
{
ByteBuffer buf = ByteBuffer.allocate( 16 );
long[] header = readAndAssertLogHeader( buf, logChannel, version );
committedTx = header[1];
logHeaderCache.put( version, committedTx );
}
finally
{
logChannel.close();
}
}
if ( committedTx < txId )
{
break;
}
version--;
}
if ( version == -1 )
{
throw new RuntimeException( "txId:" + txId + " not found in any logical log "
+ "(starting at " + logVersion
+ " and searching backwards" );
}
return new long[] { version, committedTx };
}
public long getLogicalLogLength( long version )
{
File file = new File( getFileName( version ) );
return file.exists() ? file.length() : -1;
}
public boolean hasLogicalLog( long version )
{
return new File( getFileName( version ) ).exists();
}
public boolean deleteLogicalLog( long version )
{
File file = new File(getFileName( version ) );
return file.exists() ? FileUtils.deleteFile( file ) : false;
}
private class LogApplier
{
private final ReadableByteChannel byteChannel;
private LogEntry.Start startEntry;
private LogEntry.Commit commitEntry;
LogApplier( ReadableByteChannel byteChannel )
{
this.byteChannel = byteChannel;
}
boolean readAndWriteAndApplyEntry( int newXidIdentifier ) throws IOException
{
LogEntry entry = LogIoUtils.readEntry( sharedBuffer, byteChannel, cf );
if ( entry != null )
{
entry.setIdentifier( newXidIdentifier );
if ( entry instanceof LogEntry.Commit )
{
commitEntry = (LogEntry.Commit) entry;
// msgLog.logMessage( "Applying external tx: " + ((LogEntry.Commit) entry).getTxId(), true );
}
else if ( entry instanceof LogEntry.Start )
{
startEntry = (LogEntry.Start) entry;
}
LogIoUtils.writeLogEntry( entry, writeBuffer );
applyEntry( entry );
return true;
}
return false;
}
}
private long[] readLogHeader( ReadableByteChannel source, String message ) throws IOException
{
try
{
return LogIoUtils.readLogHeader( sharedBuffer, source, true );
}
catch ( IllegalLogFormatException e )
{
msgLog.logMessage( message, e );
throw e;
}
}
public synchronized void applyTransactionWithoutTxId( ReadableByteChannel byteChannel,
long nextTxId, int masterId ) throws IOException
{
if ( nextTxId != (xaTf.getLastCommittedTx() + 1) )
{
throw new IllegalStateException( "Tried to apply tx " +
nextTxId + " but expected transaction " +
(xaTf.getCurrentVersion() + 1) );
}
logRecoveryMessage( "applyTxWithoutTxId log version: " + logVersion +
", committing tx=" + nextTxId + ") @ pos " + writeBuffer.getFileChannelPosition() );
long logEntriesFound = 0;
scanIsComplete = false;
LogApplier logApplier = new LogApplier( byteChannel );
int xidIdent = getNextIdentifier();
long startEntryPosition = writeBuffer.getFileChannelPosition();
while ( logApplier.readAndWriteAndApplyEntry( xidIdent ) )
{
logEntriesFound++;
}
byteChannel.close();
LogEntry.Start startEntry = logApplier.startEntry;
if ( startEntry == null )
{
throw new IOException( "Unable to find start entry" );
}
startEntry.setStartPosition( startEntryPosition );
// System.out.println( "applyTxWithoutTxId#before 1PC @ pos: " + writeBuffer.getFileChannelPosition() );
LogEntry.OnePhaseCommit commit = new LogEntry.OnePhaseCommit(
xidIdent, nextTxId, masterId );
LogIoUtils.writeLogEntry( commit, writeBuffer );
// need to manually force since xaRm.commit will not do it (transaction marked as recovered)
writeBuffer.force();
Xid xid = startEntry.getXid();
try
{
XaTransaction xaTx = xaRm.getXaTransaction( xid );
xaTx.setCommitTxId( nextTxId );
xaRm.commit( xid, true );
LogEntry doneEntry = new LogEntry.Done( startEntry.getIdentifier() );
LogIoUtils.writeLogEntry( doneEntry, writeBuffer );
xidIdentMap.remove( startEntry.getIdentifier() );
recoveredTxMap.remove( startEntry.getIdentifier() );
cacheTxStartPosition( nextTxId, masterId, startEntry );
}
catch ( XAException e )
{
throw new IOException( e );
}
// LogEntry.Done done = new LogEntry.Done( entry.getIdentifier() );
// LogIoUtils.writeLogEntry( done, writeBuffer );
// xaTf.setLastCommittedTx( nextTxId ); // done in doCommit
scanIsComplete = true;
// log.info( "Tx[" + nextTxId + "] " + " applied successfully." );
logRecoveryMessage( "Applied external tx and generated tx id=" + nextTxId );
checkLogRotation();
// System.out.println( "applyTxWithoutTxId#end @ pos: " + writeBuffer.getFileChannelPosition() );
}
public synchronized void applyTransaction( ReadableByteChannel byteChannel )
throws IOException
{
// System.out.println( "applyFullTx#start @ pos: " + writeBuffer.getFileChannelPosition() );
long logEntriesFound = 0;
scanIsComplete = false;
LogApplier logApplier = new LogApplier( byteChannel );
int xidIdent = getNextIdentifier();
long startEntryPosition = writeBuffer.getFileChannelPosition();
boolean successfullyApplied = false;
try
{
while ( logApplier.readAndWriteAndApplyEntry( xidIdent ) )
{
logEntriesFound++;
}
successfullyApplied = true;
}
finally
{
if ( !successfullyApplied && logApplier.startEntry != null )
{ // Unmap this identifier if tx not applied correctly
xidIdentMap.remove( xidIdent );
try
{
xaRm.forget( logApplier.startEntry.getXid() );
}
catch ( XAException e )
{
throw new IOException( e );
}
}
}
byteChannel.close();
scanIsComplete = true;
LogEntry.Start startEntry = logApplier.startEntry;
if ( startEntry == null )
{
throw new IOException( "Unable to find start entry" );
}
startEntry.setStartPosition( startEntryPosition );
cacheTxStartPosition( logApplier.commitEntry.getTxId(), logApplier.commitEntry.getMasterId(), startEntry );
// System.out.println( "applyFullTx#end @ pos: " + writeBuffer.getFileChannelPosition() );
checkLogRotation();
}
private String getLog1FileName()
{
return fileName + ".1";
}
private String getLog2FileName()
{
return fileName + ".2";
}
/**
* @return the last tx in the produced log
* @throws IOException I/O error.
*/
public synchronized long rotate() throws IOException
{
// if ( writeBuffer.getFileChannelPosition() == LogIoUtils.LOG_HEADER_SIZE ) return xaTf.getLastCommittedTx();
xaTf.flushAll();
String newLogFile = getLog2FileName();
String currentLogFile = getLog1FileName();
char newActiveLog = LOG2;
long currentVersion = xaTf.getCurrentVersion();
String oldCopy = getFileName( currentVersion );
if ( currentLog == CLEAN || currentLog == LOG2 )
{
newActiveLog = LOG1;
newLogFile = getLog1FileName();
currentLogFile = getLog2FileName();
}
else
{
assert currentLog == LOG1;
}
assertFileDoesntExist( newLogFile, "New log file" );
assertFileDoesntExist( oldCopy, "Copy log file" );
// System.out.println( " ---- Performing rotate on " + currentLogFile + " -----" );
// DumpLogicalLog.main( new String[] { currentLogFile } );
// System.out.println( " ----- end ----" );
msgLog.logMessage( "Rotating [" + currentLogFile + "] @ version=" +
currentVersion + " to " + newLogFile + " from position " +
writeBuffer.getFileChannelPosition(), true );
long endPosition = writeBuffer.getFileChannelPosition();
writeBuffer.force();
FileChannel newLog = new RandomAccessFile(
newLogFile, "rw" ).getChannel();
long lastTx = xaTf.getLastCommittedTx();
LogIoUtils.writeLogHeader( sharedBuffer, (currentVersion + 1), lastTx );
previousLogLastCommittedTx = lastTx;
if ( newLog.write( sharedBuffer ) != 16 )
{
throw new IOException( "Unable to write log version to new" );
}
long pos = fileChannel.position();
fileChannel.position( 0 );
readAndAssertLogHeader( sharedBuffer, fileChannel, currentVersion );
fileChannel.position( pos );
if ( xidIdentMap.size() > 0 )
{
long firstEntryPosition = getFirstStartEntry( endPosition );
fileChannel.position( firstEntryPosition );
msgLog.logMessage( "Rotate log first start entry @ pos=" +
firstEntryPosition );
}
LogEntry entry;
// Set<Integer> startEntriesWritten = new HashSet<Integer>();
LogBuffer newLogBuffer = instantiateCorrectWriteBuffer( newLog );
while ((entry = LogIoUtils.readEntry( sharedBuffer, fileChannel, cf )) != null )
{
if ( xidIdentMap.get( entry.getIdentifier() ) != null )
{
if ( entry instanceof LogEntry.Start )
{
LogEntry.Start startEntry = (LogEntry.Start) entry;
startEntry.setStartPosition( newLogBuffer.getFileChannelPosition() ); // newLog.position() );
// overwrite old start entry with new that has updated position
xidIdentMap.put( startEntry.getIdentifier(), startEntry );
// startEntriesWritten.add( entry.getIdentifier() );
}
else if ( entry instanceof LogEntry.Commit )
{
LogEntry.Start startEntry = xidIdentMap.get( entry.getIdentifier() );
LogEntry.Commit commitEntry = (LogEntry.Commit) entry;
TxPosition oldPos = txStartPositionCache.get( commitEntry.getTxId() );
TxPosition newPos = cacheTxStartPosition( commitEntry.getTxId(), commitEntry.getMasterId(), startEntry, logVersion+1 );
msgLog.logMessage( "Updated tx " + ((LogEntry.Commit) entry ).getTxId() +
" from " + oldPos + " to " + newPos );
}
// if ( !startEntriesWritten.contains( entry.getIdentifier() ) )
// {
// throw new IOException( "Unable to rotate log since start entry for identifier[" +
// entry.getIdentifier() + "] not written" );
// }
LogIoUtils.writeLogEntry( entry, newLogBuffer );
}
}
newLogBuffer.force();
newLog.position( newLogBuffer.getFileChannelPosition() );
msgLog.logMessage( "Rotate: old log scanned, newLog @ pos=" +
newLog.position(), true );
newLog.force( false );
releaseCurrentLogFile();
setActiveLog( newActiveLog );
if ( keepLogs )
{
renameLogFileToRightVersion( currentLogFile, endPosition );
}
else
{
deleteLogFile( currentLogFile );
}
xaTf.getAndSetNewVersion();
this.logVersion = xaTf.getCurrentVersion();
if ( xaTf.getCurrentVersion() != ( currentVersion + 1 ) )
{
throw new IOException( "version change failed" );
}
fileChannel = newLog;
logHeaderCache.put( logVersion, lastTx );
instantiateCorrectWriteBuffer();
msgLog.logMessage( "Log rotated, newLog @ pos=" +
writeBuffer.getFileChannelPosition() + " and version " + logVersion, true );
return lastTx;
}
private void assertFileDoesntExist( String file, String description ) throws IOException
{
if ( new File( file ).exists() )
{
throw new IOException( description + ": " + file + " already exist" );
}
}
private long getFirstStartEntry( long endPosition )
{
long firstEntryPosition = endPosition;
for ( LogEntry.Start entry : xidIdentMap.values() )
{
if ( entry.getStartPosition() < firstEntryPosition )
{
assert entry.getStartPosition() > 0;
firstEntryPosition = entry.getStartPosition();
}
}
return firstEntryPosition;
}
private void setActiveLog( char c ) throws IOException
{
if ( c != CLEAN && c != LOG1 && c != LOG2 )
{
throw new IllegalArgumentException( "Log must be either clean, " +
"1 or 2" );
}
if ( c == currentLog )
{
throw new IllegalStateException( "Log should not be equal to " +
"current " + currentLog );
}
ByteBuffer bb = ByteBuffer.wrap( new byte[4] );
bb.asCharBuffer().put( c ).flip();
FileChannel fc = new RandomAccessFile( fileName + ".active" ,
"rw" ).getChannel();
int wrote = fc.write( bb );
if ( wrote != 4 )
{
throw new IllegalStateException( "Expected to write 4 -> " + wrote );
}
fc.force( false );
fc.close();
currentLog = c;
}
/*
* Only call this is there's an explicit property set to control it.
* Other wise depend on the default behaviour.
*/
public void setKeepLogs( boolean keep )
{
this.keepLogs = keep;
}
private boolean hasPreviousLogs()
{
File fileNameFile = new File( fileName );
File logDirectory = fileNameFile.getParentFile();
if ( !logDirectory.exists() ) return false;
Pattern logFilePattern = getHistoryFileNamePattern();
for ( File file : logDirectory.listFiles() )
{
if ( logFilePattern.matcher( file.getName() ).find() ) return true;
}
return false;
}
public boolean isLogsKept()
{
return this.keepLogs;
}
public void setAutoRotateLogs( boolean autoRotate )
{
this.autoRotate = autoRotate;
}
public boolean isLogsAutoRotated()
{
return this.autoRotate;
}
public void setLogicalLogTargetSize( long size )
{
this.rotateAtSize = size;
}
public long getLogicalLogTargetSize()
{
return this.rotateAtSize;
}
public String getFileName( long version )
{
return fileName + ".v" + version;
}
public String getBaseFileName()
{
return fileName;
}
public Pattern getHistoryFileNamePattern()
{
return getHistoryFileNamePattern( new File( fileName ).getName() );
}
public static Pattern getHistoryFileNamePattern( String baseFileName )
{
return Pattern.compile( baseFileName + "\\.v\\d+" );
}
public static long getHistoryLogVersion( File historyLogFile )
{ // Get version based on the name
String name = historyLogFile.getName();
String toFind = ".v";
int index = name.lastIndexOf( toFind );
if ( index == -1 ) throw new RuntimeException( "Invalid log file '" + historyLogFile + "'" );
return Integer.parseInt( name.substring( index + toFind.length() ) );
}
public boolean wasNonClean()
{
return nonCleanShutdown;
}
private static class TxPosition
{
final long version;
final int masterId;
final int identifier;
final long position;
private TxPosition( long version, int masterId, int identifier, long position )
{
this.version = version;
this.masterId = masterId;
this.identifier = identifier;
this.position = position;
}
public boolean earlierThan( TxPosition other )
{
if ( version < other.version ) return true;
if ( version > other.version ) return false;
return position < other.position;
}
@Override
public String toString()
{
return "TxPosition[version:" + version + ", pos:" + position + "]";
}
}
private static interface LogEntryCollector
{
LogEntry collect( LogEntry entry, LogBuffer target ) throws IOException;
boolean hasInFutureQueue();
int getIdentifier();
}
private static class KnownIdentifierCollector implements LogEntryCollector
{
private final int identifier;
KnownIdentifierCollector( int identifier )
{
this.identifier = identifier;
}
public int getIdentifier()
{
return identifier;
}
public LogEntry collect( LogEntry entry, LogBuffer target ) throws IOException
{
if ( entry.getIdentifier() == identifier )
{
if ( target != null )
{
LogIoUtils.writeLogEntry( entry, target );
}
return entry;
}
return null;
}
@Override
public boolean hasInFutureQueue()
{
return false;
}
}
private static class KnownTxIdCollector implements LogEntryCollector
{
private final Map<Integer,List<LogEntry>> transactions = new HashMap<Integer,List<LogEntry>>();
private final long startTxId;
private int identifier;
private final Map<Long, List<LogEntry>> futureQueue = new HashMap<Long, List<LogEntry>>();
private long nextExpectedTxId;
KnownTxIdCollector( long startTxId )
{
this.startTxId = startTxId;
this.nextExpectedTxId = startTxId;
}
public int getIdentifier()
{
return identifier;
}
@Override
public boolean hasInFutureQueue()
{
return futureQueue.containsKey( nextExpectedTxId );
}
public LogEntry collect( LogEntry entry, LogBuffer target ) throws IOException
{
if ( futureQueue.containsKey( nextExpectedTxId ) )
{
List<LogEntry> list = futureQueue.remove( nextExpectedTxId++ );
writeToBuffer( list, target );
return commitEntryOf( list );
}
// boolean interesting = false;
if ( entry instanceof LogEntry.Start )
{
List<LogEntry> list = new LinkedList<LogEntry>();
list.add( entry );
transactions.put( entry.getIdentifier(), list );
}
else if ( entry instanceof LogEntry.Commit )
{
long commitTxId = ((LogEntry.Commit) entry).getTxId();
if ( commitTxId < startTxId ) return null;
// interesting = true;
identifier = entry.getIdentifier();
List<LogEntry> entries = transactions.get( identifier );
if ( entries == null ) return null;
entries.add( entry );
if ( nextExpectedTxId != startTxId )
{ // Have returned some previous tx
// If we encounter an already extracted tx in the middle of the stream
// then just ignore it. This can happen when we do log rotation,
// where records are copied over from the active log to the new.
if ( commitTxId < nextExpectedTxId ) return null;
}
if ( commitTxId != nextExpectedTxId )
{ // There seems to be a hole in the tx stream, or out-of-ordering
futureQueue.put( commitTxId, entries );
return null;
}
writeToBuffer( entries, target );
nextExpectedTxId = commitTxId+1;
return entry;
}
else if ( entry instanceof LogEntry.Command || entry instanceof LogEntry.Prepare )
{
List<LogEntry> list = transactions.get( entry.getIdentifier() );
// Since we can start reading at any position in the log it might be the case
// that we come across a record which corresponding start record resides
// before the position we started reading from. If that should be the case
// then skip it since it isn't an important record for us here.
if ( list != null )
{
list.add( entry );
}
}
else if ( entry instanceof LogEntry.Done )
{
transactions.remove( entry.getIdentifier() );
}
else
{
throw new RuntimeException( "Unknown entry: " + entry );
}
return null;
}
private LogEntry commitEntryOf( List<LogEntry> list )
{
for ( LogEntry entry : list )
{
if ( entry instanceof LogEntry.Commit ) return entry;
}
throw new RuntimeException( "No commit entry in " + list );
}
private void writeToBuffer( List<LogEntry> entries, LogBuffer target ) throws IOException
{
if ( target != null )
{
for ( LogEntry entry : entries )
{
LogIoUtils.writeLogEntry( entry, target );
}
}
}
}
public long getCurrentLogVersion()
{
return logVersion;
}
}