/*
* Copyright (c) 2002-2009 "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 Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero 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.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;
import org.neo4j.graphdb.TransactionFailureException;
import org.neo4j.kernel.Config;
import org.neo4j.kernel.impl.transaction.XidImpl;
import org.neo4j.kernel.impl.util.ArrayMap;
import org.neo4j.kernel.impl.util.FileUtils;
/**
* <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 Logger log;
// empty record due to memory mapped file
private static final byte EMPTY = (byte) 0;
// tx has started
private static final byte TX_START = (byte) 1;
// tx has been prepared
private static final byte TX_PREPARE = (byte) 2;
// a XaCommand in a transaction
private static final byte COMMAND = (byte) 3;
// done, either a read only tx or rolledback/forget
private static final byte DONE = (byte) 4;
// tx one-phase commit
private static final byte TX_1P_COMMIT = (byte) 5;
// tx two-phase commit
private static final byte TX_2P_COMMIT = (byte) 6;
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 buffer;
private LogBuffer writeBuffer = null;
private long logVersion = 0;
private ArrayMap<Integer,StartEntry> xidIdentMap =
new ArrayMap<Integer,StartEntry>( 4, false, true );
private Map<Integer,XaTransaction> recoveredTxMap =
new HashMap<Integer,XaTransaction>();
private int nextIdentifier = 1;
private boolean scanIsComplete = 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 = 10*1024*1024; // 10MB
private boolean backupSlave = false;
private boolean useMemoryMapped = true;
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.useMemoryMapped = getMemoryMapped( config );
log = Logger.getLogger( this.getClass().getName() + "/" + fileName );
buffer = ByteBuffer.allocateDirect( 9 + Xid.MAXGTRIDSIZE
+ Xid.MAXBQUALSIZE * 10 );
}
private boolean getMemoryMapped( Map<Object,Object> config )
{
if ( config != null )
{
String value = (String) config.get( Config.USE_MEMORY_MAPPED_BUFFERS );
if ( value != null && value.toLowerCase().equals( "false" ) )
{
return false;
}
}
return true;
}
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( fileName + ".1" );
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();
File copy = new File( fileName + ".copy" );
if ( copy.exists() )
{
if ( !copy.delete() )
{
log.warning( "Unable to delete " + copy.getName() );
}
}
if ( c == CLEAN )
{
// clean
String newLog = fileName + ".1";
File file = new File( newLog );
if ( file.exists() )
{
fixCleanKill( newLog );
}
file = new File( fileName + ".2" );
if ( file.exists() )
{
fixCleanKill( fileName + ".2" );
}
open( newLog );
setActiveLog( LOG1 );
}
else if ( c == LOG1 )
{
String newLog = fileName + ".1";
if ( !new File( newLog ).exists() )
{
throw new IllegalStateException(
"Active marked as 1 but no " + newLog + " exist" );
}
currentLog = LOG1;
File otherLog = new File( fileName + ".2" );
if ( otherLog.exists() )
{
if ( !otherLog.delete() )
{
log.warning( "Unable to delete " + copy.getName() );
}
}
open( newLog );
}
else if ( c == LOG2 )
{
String newLog = fileName + ".2";
if ( !new File( newLog ).exists() )
{
throw new IllegalStateException(
"Active marked as 2 but no " + newLog + " exist" );
}
File otherLog = new File( fileName + ".1" );
if ( otherLog.exists() )
{
if ( !otherLog.delete() )
{
log.warning( "Unable to delete " + copy.getName() );
}
}
currentLog = LOG2;
open( newLog );
}
else
{
throw new IllegalStateException( "Unknown active log: " + c );
}
}
if ( !useMemoryMapped )
{
writeBuffer = new DirectMappedLogBuffer( fileChannel );
}
else
{
writeBuffer = new MemoryMappedLogBuffer( fileChannel );
}
}
private void fixCleanKill( String fileName ) throws IOException
{
File file = new File( fileName );
if ( !keepLogs )
{
if ( !file.delete() )
{
throw new IllegalStateException(
"Active marked as clean and unable to delete log " +
fileName );
}
}
else
{
renameCurrentLogFileAndIncrementVersion( fileName, file.length() );
}
}
private void open( String fileToOpen ) throws IOException
{
fileChannel = new RandomAccessFile( fileToOpen, "rw" ).getChannel();
if ( fileChannel.size() != 0 )
{
doInternalRecovery( fileToOpen );
}
else
{
logVersion = xaTf.getCurrentVersion();
buffer.clear();
buffer.putLong( logVersion );
buffer.flip();
fileChannel.write( buffer );
scanIsComplete = 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
{
if ( backupSlave )
{
throw new XAException( "Resource is configured as backup slave, " +
"no new transactions can be started for " + fileName + "." +
currentLog );
}
int xidIdent = getNextIdentifier();
try
{
byte globalId[] = xid.getGlobalTransactionId();
byte branchId[] = xid.getBranchQualifier();
int formatId = xid.getFormatId();
long position = writeBuffer.getFileChannelPosition();
writeBuffer.put( TX_START ).put( (byte) globalId.length ).put(
(byte) branchId.length ).put( globalId ).put( branchId )
.putInt( xidIdent ).putInt( formatId );
xidIdentMap.put( xidIdent, new StartEntry( xid, position ) );
}
catch ( IOException e )
{
throw new XAException( "Logical log couldn't start transaction: "
+ e );
}
return xidIdent;
}
private boolean readTxStartEntry() throws IOException
{
// get the global id
long position = fileChannel.position();
buffer.clear();
buffer.limit( 1 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
byte globalIdLength = buffer.get();
// get the branchId id
buffer.clear();
buffer.limit( 1 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
byte branchIdLength = buffer.get();
byte globalId[] = new byte[globalIdLength];
ByteBuffer tmpBuffer = ByteBuffer.wrap( globalId );
if ( fileChannel.read( tmpBuffer ) != globalId.length )
{
return false;
}
byte branchId[] = new byte[branchIdLength];
tmpBuffer = ByteBuffer.wrap( branchId );
if ( fileChannel.read( tmpBuffer ) != branchId.length )
{
return false;
}
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int identifier = buffer.getInt();
if ( identifier >= nextIdentifier )
{
nextIdentifier = (identifier + 1);
}
// get the format id
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int formatId = buffer.getInt();
// re-create the transaction
Xid xid = new XidImpl( globalId, branchId, formatId );
xidIdentMap.put( identifier, new StartEntry( xid, position ) );
XaTransaction xaTx = xaTf.create( identifier );
xaTx.setRecovered();
recoveredTxMap.put( identifier, xaTx );
xaRm.injectStart( xid, xaTx );
return true;
}
// [TX_PREPARE][identifier]
public synchronized void prepare( int identifier ) throws XAException
{
assert xidIdentMap.get( identifier ) != null;
try
{
writeBuffer.put( TX_PREPARE ).putInt( identifier );
writeBuffer.force();
}
catch ( IOException e )
{
throw new XAException( "Logical log unable to mark prepare ["
+ identifier + "] " + e );
}
}
private boolean readTxPrepareEntry() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
return false;
}
Xid xid = entry.getXid();
if ( xaRm.injectPrepare( xid ) )
{
// read only we can remove
xidIdentMap.remove( identifier );
recoveredTxMap.remove( identifier );
}
return true;
}
// [TX_1P_COMMIT][identifier]
public synchronized void commitOnePhase( int identifier )
throws XAException
{
assert xidIdentMap.get( identifier ) != null;
try
{
writeBuffer.put( TX_1P_COMMIT ).putInt( identifier );
writeBuffer.force();
}
catch ( IOException e )
{
throw new XAException( "Logical log unable to mark 1P-commit ["
+ identifier + "] " + e );
}
}
private boolean readTxOnePhaseCommit() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
return false;
}
Xid xid = entry.getXid();
try
{
xaRm.injectOnePhaseCommit( xid );
}
catch ( XAException e )
{
e.printStackTrace();
throw new IOException( e.getMessage() );
}
return true;
}
// [DONE][identifier]
public synchronized void done( int identifier ) throws XAException
{
if ( backupSlave )
{
return;
}
assert xidIdentMap.get( identifier ) != null;
try
{
writeBuffer.put( DONE ).putInt( identifier );
xidIdentMap.remove( identifier );
}
catch ( IOException e )
{
throw 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
{
buffer.clear();
buffer.put( DONE ).putInt( identifier );
buffer.flip();
fileChannel.write( buffer );
xidIdentMap.remove( identifier );
}
private boolean readDoneEntry() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
return false;
}
Xid xid = entry.getXid();
xaRm.pruneXid( xid );
xidIdentMap.remove( identifier );
recoveredTxMap.remove( identifier );
return true;
}
// [TX_2P_COMMIT][identifier]
public synchronized void commitTwoPhase( int identifier ) throws XAException
{
assert xidIdentMap.get( identifier ) != null;
try
{
writeBuffer.put( TX_2P_COMMIT ).putInt( identifier );
writeBuffer.force();
}
catch ( IOException e )
{
throw new XAException( "Logical log unable to mark 2PC ["
+ identifier + "] " + e );
}
}
private boolean readTxTwoPhaseCommit() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
return false;
}
Xid xid = entry.getXid();
if ( xid == null )
{
return false;
}
try
{
xaRm.injectTwoPhaseCommit( xid );
}
catch ( XAException e )
{
e.printStackTrace();
throw new IOException( e.getMessage() );
}
return true;
}
// [COMMAND][identifier][COMMAND_DATA]
public synchronized void writeCommand( XaCommand command, int identifier )
throws IOException
{
checkLogRotation();
assert xidIdentMap.get( identifier ) != null;
writeBuffer.put( COMMAND ).putInt( identifier );
command.writeToFile( writeBuffer ); // fileChannel, buffer );
}
private boolean readCommandEntry() throws IOException
{
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int identifier = buffer.getInt();
XaCommand command = cf.readCommand( fileChannel, buffer );
if ( command == null )
{
// readCommand returns null if full command couldn't be loaded
return false;
}
command.setRecovered();
XaTransaction xaTx = recoveredTxMap.get( identifier );
xaTx.injectCommand( command );
return true;
}
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 renameCurrentLogFileAndIncrementVersion( String logFileName,
long endPosition ) throws IOException
{
File file = new File( logFileName );
if ( !file.exists() )
{
throw new IOException( "Logical log[" + logFileName +
"] not found" );
}
String newName = fileName + ".v" + xaTf.getAndSetNewVersion();
File newFile = new File( newName );
boolean renamed = FileUtils.renameFile( file, newFile );
if ( !renamed )
{
throw new IOException( "Failed to rename log to: " + newName );
}
else
{
try
{
FileChannel channel = new RandomAccessFile( newName,
"rw" ).getChannel();
FileUtils.truncateFile( channel, endPosition );
}
catch ( IOException e )
{
log.log( Level.WARNING,
"Failed to truncate log at correct size", e );
}
}
}
private void deleteCurrentLogFile( 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();
writeBuffer = null;
}
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();
writeBuffer = null;
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 || backupSlave )
{
if ( logWas == CLEAN )
{
// special case going from old xa version with no log rotation
// and we started with a recovery
deleteCurrentLogFile( fileName );
}
else
{
deleteCurrentLogFile( fileName + "." + logWas );
}
}
else
{
renameCurrentLogFileAndIncrementVersion( fileName + "." +
logWas, endPosition );
}
}
private void doInternalRecovery( String logFileName ) throws IOException
{
log.info( "Non clean shutdown detected on log [" + logFileName +
"]. Recovery started ..." );
// get log creation time
buffer.clear();
buffer.limit( 8 );
if ( fileChannel.read( buffer ) != 8 )
{
log.info( "Unable to read timestamp information, "
+ "no records in logical log." );
fileChannel.close();
boolean success = FileUtils.renameFile( new File( logFileName ),
new File( logFileName + "_unknown_timestamp_" +
System.currentTimeMillis() + ".log" ) );
assert success;
fileChannel = new RandomAccessFile( logFileName,
"rw" ).getChannel();
return;
}
buffer.flip();
logVersion = buffer.getLong();
log.fine( "Logical log version: " + logVersion );
long logEntriesFound = 0;
long lastEntryPos = fileChannel.position();
while ( readEntry() )
{
logEntriesFound++;
lastEntryPos = fileChannel.position();
}
// make sure we overwrite any broken records
fileChannel.position( lastEntryPos );
// zero out the slow way since windows don't support truncate very well
buffer.clear();
while ( buffer.hasRemaining() )
{
buffer.put( (byte)0 );
}
buffer.flip();
long endPosition = fileChannel.size();
do
{
long bytesLeft = fileChannel.size() - fileChannel.position();
if ( bytesLeft < buffer.capacity() )
{
buffer.limit( (int) bytesLeft );
}
fileChannel.write( buffer );
buffer.flip();
} while ( fileChannel.position() < endPosition );
fileChannel.position( lastEntryPos );
scanIsComplete = true;
log.fine( "Internal recovery completed, scanned " + logEntriesFound
+ " log entries." );
xaRm.checkXids();
if ( xidIdentMap.size() == 0 )
{
log.fine( "Recovery completed." );
}
else
{
log.fine( "[" + logFileName + "] Found " + xidIdentMap.size()
+ " prepared 2PC transactions." );
for ( StartEntry entry : xidIdentMap.values() )
{
log.fine( "[" + logFileName + "] 2PC xid[" +
entry.getXid() + "]" );
}
}
recoveredTxMap.clear();
}
// for testing, do not use!
void reset()
{
xidIdentMap.clear();
recoveredTxMap.clear();
}
private boolean readEntry() throws IOException
{
buffer.clear();
buffer.limit( 1 );
if ( fileChannel.read( buffer ) != buffer.limit() )
{
// ok no more entries we're done
return false;
}
buffer.flip();
byte entry = buffer.get();
switch ( entry )
{
case TX_START:
return readTxStartEntry();
case TX_PREPARE:
return readTxPrepareEntry();
case TX_1P_COMMIT:
return readTxOnePhaseCommit();
case TX_2P_COMMIT:
return readTxTwoPhaseCommit();
case COMMAND:
return readCommandEntry();
case DONE:
return readDoneEntry();
case EMPTY:
fileChannel.position( fileChannel.position() - 1 );
return false;
default:
throw new IOException( "Internal recovery failed, "
+ "unknown log entry[" + entry + "]" );
}
}
private 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
{
String name = fileName + ".v" + version;
if ( !new File( name ).exists() )
{
throw new IOException( "No such log version:" + version );
}
return new RandomAccessFile( name, "r" ).getChannel();
}
public long getLogicalLogLength( long version )
{
String name = fileName + ".v" + version;
File file = new File( name );
if ( !file.exists() )
{
return -1;
}
return file.length();
}
public boolean hasLogicalLog( long version )
{
String name = fileName + ".v" + version;
return new File( name ).exists();
}
public boolean deleteLogicalLog( long version )
{
String name = fileName + ".v" + version;
File file = new File(name );
if ( file.exists() )
{
return FileUtils.deleteFile( file );
}
return false;
}
public void makeBackupSlave()
{
if ( xidIdentMap.size() > 0 )
{
throw new IllegalStateException( "There are active transactions" );
}
backupSlave = true;
}
private static class LogApplier
{
private final ReadableByteChannel byteChannel;
private final ByteBuffer buffer;
private final XaTransactionFactory xaTf;
private final XaResourceManager xaRm;
private final XaCommandFactory xaCf;
private final ArrayMap<Integer,StartEntry> xidIdentMap;
private final Map<Integer,XaTransaction> recoveredTxMap;
LogApplier( ReadableByteChannel byteChannel, ByteBuffer buffer,
XaTransactionFactory xaTf, XaResourceManager xaRm,
XaCommandFactory xaCf, ArrayMap<Integer,StartEntry> xidIdentMap,
Map<Integer,XaTransaction> recoveredTxMap )
{
this.byteChannel = byteChannel;
this.buffer = buffer;
this.xaTf = xaTf;
this.xaRm = xaRm;
this.xaCf = xaCf;
this.xidIdentMap = xidIdentMap;
this.recoveredTxMap = recoveredTxMap;
}
boolean readAndApplyEntry() throws IOException
{
buffer.clear();
buffer.limit( 1 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
// ok no more entries we're done
return false;
}
buffer.flip();
byte entry = buffer.get();
switch ( entry )
{
case TX_START:
readTxStartEntry();
return true;
case TX_PREPARE:
readTxPrepareEntry();
return true;
case TX_1P_COMMIT:
readAndApplyTxOnePhaseCommit();
return true;
case TX_2P_COMMIT:
readAndApplyTxTwoPhaseCommit();
return true;
case COMMAND:
readCommandEntry();
return true;
case DONE:
readDoneEntry();
return true;
case EMPTY:
return false;
default:
throw new IOException( "Internal recovery failed, "
+ "unknown log entry[" + entry + "]" );
}
}
private void readTxStartEntry() throws IOException
{
// get the global id
buffer.clear();
buffer.limit( 1 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx start entry" );
}
buffer.flip();
byte globalIdLength = buffer.get();
// get the branchId id
buffer.clear();
buffer.limit( 1 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx start entry" );
}
buffer.flip();
byte branchIdLength = buffer.get();
byte globalId[] = new byte[globalIdLength];
ByteBuffer tmpBuffer = ByteBuffer.wrap( globalId );
if ( byteChannel.read( tmpBuffer ) != globalId.length )
{
throw new IOException( "Unable to read tx start entry" );
}
byte branchId[] = new byte[branchIdLength];
tmpBuffer = ByteBuffer.wrap( branchId );
if ( byteChannel.read( tmpBuffer ) != branchId.length )
{
throw new IOException( "Unable to read tx start entry" );
}
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx start entry" );
}
buffer.flip();
int identifier = buffer.getInt();
// get the format id
buffer.clear();
buffer.limit( 4 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx start entry" );
}
buffer.flip();
int formatId = buffer.getInt();
// re-create the transaction
Xid xid = new XidImpl( globalId, branchId, formatId );
xidIdentMap.put( identifier, new StartEntry( xid, -1 ) );
XaTransaction xaTx = xaTf.create( identifier );
xaTx.setRecovered();
recoveredTxMap.put( identifier, xaTx );
xaRm.injectStart( xid, xaTx );
}
private void readTxPrepareEntry() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx prepare entry" );
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
throw new IOException( "Unable to read tx prepeare entry" );
}
Xid xid = entry.getXid();
if ( xaRm.injectPrepare( xid ) )
{
// read only, we can remove
xidIdentMap.remove( identifier );
recoveredTxMap.remove( identifier );
}
}
private void readAndApplyTxOnePhaseCommit() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx 1PC entry" );
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
throw new IOException( "Unable to read tx prepeare entry" );
}
Xid xid = entry.getXid();
try
{
xaRm.commit( xid, true );
}
catch ( XAException e )
{
e.printStackTrace();
throw new IOException( e.getMessage() );
}
}
private void readAndApplyTxTwoPhaseCommit() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx 2PC entry" );
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
throw new IOException( "Unable to read tx prepeare entry" );
}
Xid xid = entry.getXid();
try
{
xaRm.commit( xid, true );
}
catch ( XAException e )
{
e.printStackTrace();
throw new IOException( e.getMessage() );
}
}
private void readCommandEntry() throws IOException
{
buffer.clear();
buffer.limit( 4 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
throw new IOException( "Unable to read tx command entry" );
}
buffer.flip();
int identifier = buffer.getInt();
XaCommand command = xaCf.readCommand( byteChannel, buffer );
if ( command == null )
{
throw new IOException( "Unable to read command entry" );
}
command.setRecovered();
XaTransaction xaTx = recoveredTxMap.get( identifier );
xaTx.injectCommand( command );
}
private boolean readDoneEntry() throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 4 );
if ( byteChannel.read( buffer ) != buffer.limit() )
{
return false;
}
buffer.flip();
int identifier = buffer.getInt();
StartEntry entry = xidIdentMap.get( identifier );
if ( entry == null )
{
throw new IOException( "Unable to read tx done entry" );
}
Xid xid = entry.getXid();
xaRm.pruneXidIfExist( xid );
xidIdentMap.remove( identifier );
recoveredTxMap.remove( identifier );
return true;
}
}
public synchronized void applyLog( ReadableByteChannel byteChannel )
throws IOException
{
if ( !backupSlave )
{
throw new IllegalStateException( "This is not a backup slave" );
}
if ( xidIdentMap.size() > 0 )
{
throw new IllegalStateException( "There are active transactions" );
}
buffer.clear();
buffer.limit( 8 );
if ( byteChannel.read( buffer ) != 8 )
{
throw new IOException( "Unable to read log version" );
}
buffer.flip();
logVersion = buffer.getLong();
if ( logVersion != xaTf.getCurrentVersion() )
{
throw new IllegalStateException( "Tried to apply version " +
logVersion + " but expected version " +
xaTf.getCurrentVersion() );
}
log.fine( "Logical log version: " + logVersion );
long logEntriesFound = 0;
LogApplier logApplier = new LogApplier( byteChannel, buffer, xaTf, xaRm,
cf, xidIdentMap, recoveredTxMap );
while ( logApplier.readAndApplyEntry() )
{
logEntriesFound++;
}
byteChannel.close();
xaTf.flushAll();
xaTf.getAndSetNewVersion();
xaRm.reset();
log.info( "Log[" + fileName + "] version " + logVersion +
" applied successfully." );
}
public synchronized void rotate() throws IOException
{
xaTf.flushAll();
String newLogFile = fileName + ".2";
String currentLogFile = fileName + ".1";
char newActiveLog = LOG2;
long currentVersion = xaTf.getCurrentVersion();
String oldCopy = fileName + ".v" + currentVersion;
if ( currentLog == CLEAN || currentLog == LOG2 )
{
newActiveLog = LOG1;
newLogFile = fileName + ".1";
currentLogFile = fileName + ".2";
}
else
{
assert currentLog == LOG1;
}
if ( new File( newLogFile ).exists() )
{
throw new IOException( "New log file: " + newLogFile +
" already exist" );
}
if ( new File( oldCopy ).exists() )
{
throw new IOException( "Copy log file: " + oldCopy +
" already exist" );
}
long endPosition = writeBuffer.getFileChannelPosition();
writeBuffer.force();
FileChannel newLog = new RandomAccessFile(
newLogFile, "rw" ).getChannel();
buffer.clear();
buffer.putLong( currentVersion + 1 ).flip();
if ( newLog.write( buffer ) != 8 )
{
throw new IOException( "Unable to write log version to new" );
}
fileChannel.position( 0 );
buffer.clear();
buffer.limit( 8 );
if( fileChannel.read( buffer ) != 8 )
{
throw new IOException( "Verification of log version failed" );
}
buffer.flip();
long verification = buffer.getLong();
if ( verification != currentVersion )
{
throw new IOException( "Verification of log version failed, " +
" expected " + currentVersion + " got " + verification );
}
if ( xidIdentMap.size() > 0 )
{
fileChannel.position( getFirstStartEntry( endPosition ) );
}
buffer.clear();
buffer.limit( 1 );
boolean emptyHit = false;
while ( fileChannel.read( buffer ) == 1 && !emptyHit )
{
buffer.flip();
byte entry = buffer.get();
switch ( entry )
{
case TX_START:
readAndWriteTxStartEntry( newLog );
break;
case TX_PREPARE:
readAndWriteTxPrepareEntry( newLog );
break;
case TX_1P_COMMIT:
readAndWriteTxOnePhaseCommit( newLog );
break;
case TX_2P_COMMIT:
readAndWriteTxTwoPhaseCommit( newLog );
break;
case COMMAND:
readAndWriteCommandEntry( newLog );
break;
case DONE:
readAndVerifyDoneEntry();
break;
case EMPTY:
emptyHit = true;
break;
default:
throw new IOException( "Log rotation failed, "
+ "unknown log entry[" + entry + "]" );
}
buffer.clear();
buffer.limit( 1 );
}
newLog.force( false );
releaseCurrentLogFile();
setActiveLog( newActiveLog );
if ( keepLogs )
{
renameCurrentLogFileAndIncrementVersion( currentLogFile,
endPosition );
}
else
{
deleteCurrentLogFile( currentLogFile );
xaTf.getAndSetNewVersion();
}
if ( xaTf.getCurrentVersion() != ( currentVersion + 1 ) )
{
throw new IOException( "version change failed" );
}
fileChannel = newLog;
if ( !useMemoryMapped )
{
writeBuffer = new DirectMappedLogBuffer( fileChannel );
}
else
{
writeBuffer = new MemoryMappedLogBuffer( fileChannel );
}
}
private long getFirstStartEntry( long endPosition )
{
long firstEntryPosition = endPosition;
for ( StartEntry 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;
}
// [COMMAND][identifier][COMMAND_DATA]
private void readAndWriteCommandEntry( FileChannel newLog )
throws IOException
{
buffer.clear();
buffer.put( COMMAND );
buffer.limit( 1 + 4 );
if ( fileChannel.read( buffer ) != 4 )
{
throw new IllegalStateException( "Unable to read command header" );
}
buffer.flip();
buffer.position( 1 );
int identifier = buffer.getInt();
FileChannel writeToLog = null;
if ( xidIdentMap.get( identifier ) != null )
{
writeToLog = newLog;
}
if ( writeToLog != null )
{
buffer.position( 0 );
if ( writeToLog.write( buffer ) != 5 )
{
throw new TransactionFailureException(
"Unable to write command header" );
}
}
XaCommand command = cf.readCommand( fileChannel, buffer );
if ( writeToLog != null )
{
command.writeToFile( new DirectLogBuffer( writeToLog, buffer ) );
}
}
private void readAndVerifyDoneEntry()
throws IOException
{
buffer.clear();
buffer.limit( 4 );
if ( fileChannel.read( buffer ) != 4 )
{
throw new IllegalStateException( "Unable to read done entry" );
}
buffer.flip();
int identifier = buffer.getInt();
if ( xidIdentMap.get( identifier ) != null )
{
throw new IllegalStateException( identifier +
" done entry found but still active" );
}
}
// [TX_1P_COMMIT][identifier]
private void readAndWriteTxOnePhaseCommit( FileChannel newLog )
throws IOException
{
buffer.clear();
buffer.limit( 1 + 4 );
buffer.put( TX_1P_COMMIT );
if ( fileChannel.read( buffer ) != 4 )
{
throw new IllegalStateException( "Unable to read 1P commit entry" );
}
buffer.flip();
buffer.position( 1 );
int identifier = buffer.getInt();
FileChannel writeToLog = null;
if ( xidIdentMap.get( identifier ) != null )
{
writeToLog = newLog;
}
buffer.position( 0 );
if ( writeToLog != null && writeToLog.write( buffer ) != 5 )
{
throw new TransactionFailureException(
"Unable to write 1P commit entry" );
}
}
private void readAndWriteTxTwoPhaseCommit( FileChannel newLog )
throws IOException
{
buffer.clear();
buffer.limit( 1 + 4 );
buffer.put( TX_2P_COMMIT );
if ( fileChannel.read( buffer ) != 4 )
{
throw new IllegalStateException( "Unable to read 2P commit entry" );
}
buffer.flip();
buffer.position( 1 );
int identifier = buffer.getInt();
FileChannel writeToLog = null;
if ( xidIdentMap.get( identifier ) != null )
{
// throw new IllegalStateException( identifier +
// " 2PC found but still active" );
writeToLog = newLog;
}
buffer.position( 0 );
if ( writeToLog != null && writeToLog.write( buffer ) != 5 )
{
throw new TransactionFailureException(
"Unable to write 2P commit entry" );
}
}
private void readAndWriteTxPrepareEntry( FileChannel newLog )
throws IOException
{
// get the tx identifier
buffer.clear();
buffer.limit( 1 + 4 );
buffer.put( TX_PREPARE );
if ( fileChannel.read( buffer ) != 4 )
{
throw new IllegalStateException( "Unable to read prepare entry" );
}
buffer.flip();
buffer.position( 1 );
int identifier = buffer.getInt();
FileChannel writeToLog = null;
if ( xidIdentMap.get( identifier ) != null )
{
writeToLog = newLog;
}
buffer.position( 0 );
if ( writeToLog != null && writeToLog.write( buffer ) != 5 )
{
throw new TransactionFailureException(
"Unable to write prepare entry" );
}
}
// [TX_START][xid[gid.length,bid.lengh,gid,bid]][identifier][format id]
private void readAndWriteTxStartEntry( FileChannel newLog )
throws IOException
{
// get the global id
buffer.clear();
buffer.put( TX_START );
buffer.limit( 3 );
if ( fileChannel.read( buffer ) != 2 )
{
throw new IllegalStateException(
"Unable to read tx start entry xid id lengths" );
}
buffer.flip();
buffer.position( 1 );
byte globalIdLength = buffer.get();
byte branchIdLength = buffer.get();
int xidLength = globalIdLength + branchIdLength;
buffer.limit( 3 + xidLength + 8 );
buffer.position( 3 );
if ( fileChannel.read( buffer ) != 8 + xidLength )
{
throw new IllegalStateException( "Unable to read xid" );
}
buffer.flip();
buffer.position( 3 + xidLength );
int identifier = buffer.getInt();
FileChannel writeToLog = null;
StartEntry entry = xidIdentMap.get( identifier );
if ( entry != null )
{
writeToLog = newLog;
entry.setStartPosition( newLog.position() );
}
buffer.position( 0 );
if ( writeToLog != null &&
writeToLog.write( buffer ) != 3 + 8 + xidLength )
{
throw new TransactionFailureException(
"Unable to write tx start xid" );
}
}
public void setKeepLogs( boolean keep )
{
this.keepLogs = keep;
}
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;
}
private static class StartEntry
{
private final Xid xid;
private long startEntryPosition;
StartEntry( Xid xid, long startPosition )
{
this.xid = xid;
this.startEntryPosition = startPosition;
}
Xid getXid()
{
return xid;
}
long getStartPosition()
{
return startEntryPosition;
}
void setStartPosition( long newPosition )
{
startEntryPosition = newPosition;
}
}
public String getFileName( long version )
{
return fileName + ".v" + version;
}
}