/*
* 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;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.transaction.xa.Xid;
// TODO: fixed sized logs (pre-initialize them)
// keep dangling records in memory for log switch
// batch disk forces
/**
* This class is made public for testing purposes only, do not use.
* <p>
* The {@link TxManager} uses this class to keep a transaction log for
* transaction recovery.
* <p>
*/
public class TxLog
{
private String name = null;
private FileChannel fileChannel = null;
private ByteBuffer buffer = null;
private int recordCount = 0;
public static final byte TX_START = 1;
public static final byte BRANCH_ADD = 2;
public static final byte MARK_COMMIT = 3;
public static final byte TX_DONE = 4;
/**
* Initializes a transaction log using <CODE>filename</CODE>. If the file
* isn't empty the position will be set to size of file so new records will
* be appended.
*
* @param fileName
* Filename of file to use
* @throws IOException
* If unable to open file
*/
public TxLog( String fileName ) throws IOException
{
if ( fileName == null )
{
throw new IllegalArgumentException( "Null filename" );
}
fileChannel = new RandomAccessFile( fileName, "rw" ).getChannel();
fileChannel.position( fileChannel.size() );
buffer = ByteBuffer.allocateDirect(
(3 + Xid.MAXGTRIDSIZE + Xid.MAXBQUALSIZE) * 1000 );
this.name = fileName;
}
/**
* Returns the name of the transaction log.
*/
public String getName()
{
return name;
}
/**
* Returns the number of records (one of TX_START,BRANCH_ADD,MARK_COMMIT or
* TX_DONE) written since this instance was created or truncated.
*/
public int getRecordCount()
{
return recordCount;
}
/**
* Closes the file representing the transaction log.
*/
public void close() throws IOException
{
fileChannel.close();
}
/**
* Forces the log file (with metadata). Useful when switching log.
*/
public void force() throws IOException
{
fileChannel.force( true );
}
/**
* Truncates the file to zero size and sets the record count to zero.
*/
public synchronized void truncate() throws IOException
{
fileChannel.position( 0 );
fileChannel.truncate( 0 );
recordCount = 0;
}
/**
* Writes a <CODE>TX_START</CODE> record to the file.
*
* @param globalId
* The global id of the new transaction
* @throws IOException
* If unable to write
*/
// tx_start(byte)|gid_length(byte)|globalId
public synchronized void txStart( byte globalId[] ) throws IOException
{
if ( globalId == null )
{
throw new IllegalArgumentException( "Null parameter" );
}
buffer.clear();
buffer.put( TX_START ).put( (byte) globalId.length ).put( globalId );
buffer.flip();
fileChannel.write( buffer );
recordCount++;
}
/**
* Writes a <CODE>BRANCH_ADD</CODE> record to the file.
*
* @param globalId
* The global id of the transaction
* @param branchId
* The branch id for the enlisted resource
* @throws IOException
* If unable to write
*/
// add_branch(byte)|gid_length(byte)|bid_length(byte)|globalId|branchId
public synchronized void addBranch( byte globalId[], byte branchId[] )
throws IOException
{
if ( globalId == null )
{
throw new IllegalArgumentException( "Null global id" );
}
if ( branchId == null )
{
throw new IllegalArgumentException( "Null branch id" );
}
buffer.clear();
buffer.put( BRANCH_ADD ).put( (byte) globalId.length ).put(
(byte) branchId.length ).put( globalId ).put( branchId );
buffer.flip();
fileChannel.write( buffer );
recordCount++;
}
/**
* Writes a <CODE>MARK_COMMIT</CODE> record to the file and forces the
* file to disk.
*
* @param globalId
* The global id of the transaction
* @throws IOException
* If unable to write
*/
// mark_committing(byte)|gid_length(byte)|globalId
// forces
public synchronized void markAsCommitting( byte globalId[] )
throws IOException
{
if ( globalId == null )
{
throw new IllegalArgumentException( "Null parameter" );
}
buffer.clear();
buffer.put( MARK_COMMIT ).put( (byte) globalId.length ).put( globalId );
buffer.flip();
fileChannel.write( buffer );
fileChannel.force( false );
recordCount++;
}
/**
* Writes a <CODE>TX_DONE</CODE> record to the file.
*
* @param globalId
* The global id of the transaction completed
* @throws IOException
* If unable to write
*/
// tx_done(byte)|gid_length(byte)|globalId
public synchronized void txDone( byte globalId[] ) throws IOException
{
if ( globalId == null )
{
throw new IllegalArgumentException( "Null parameter" );
}
buffer.clear();
buffer.put( TX_DONE ).put( (byte) globalId.length ).put( globalId );
buffer.flip();
fileChannel.write( buffer );
recordCount++;
}
/**
* Made public for testing only.
* <p>
* Wraps a transaction record in the tx log file.
*/
public static class Record
{
private byte type = 0;
private byte globalId[] = null;
private byte branchId[] = null;
private int seqNr = -1;
Record( byte type, byte globalId[], byte branchId[], int seqNr )
{
if ( type < 1 || type > 4 )
{
throw new IllegalArgumentException( "Illegal type: " + type );
}
this.type = type;
this.globalId = globalId;
this.branchId = branchId;
this.seqNr = seqNr;
}
public byte getType()
{
return type;
}
public byte[] getGlobalId()
{
return globalId;
}
public byte[] getBranchId()
{
return branchId;
}
public int getSequenceNumber()
{
return seqNr;
}
public String toString()
{
XidImpl xid = new XidImpl( globalId, branchId == null ? new byte[0]
: branchId );
return "TxLogRecord[" + type + "," + xid + "," + seqNr + "]";
}
}
void writeRecord( Record record ) throws IOException
{
if ( record.getType() == TX_START )
{
txStart( record.getGlobalId() );
}
else if ( record.getType() == BRANCH_ADD )
{
addBranch( record.getGlobalId(), record.getBranchId() );
}
else if ( record.getType() == MARK_COMMIT )
{
markAsCommitting( record.getGlobalId() );
}
else
{
// TX_DONE should never be passed in here
throw new IOException( "Illegal record type[" + record.getType()
+ "]" );
}
}
/**
* Returns an array of lists, each list contains dangling records
* (transactions that han't been completed yet) grouped after global by
* transaction id.
*/
public synchronized Iterator<List<Record>> getDanglingRecords()
throws IOException
{
fileChannel.position( 0 );
buffer.clear();
fileChannel.read( buffer );
buffer.flip();
// next record position
long nextPosition = 0;
// holds possible dangling records
int seqNr = 0;
Map<Xid,List<Record>> recordMap = new HashMap<Xid,List<Record>>();
while ( buffer.hasRemaining() )
{
byte recordType = buffer.get();
if ( recordType == TX_START )
{
if ( !buffer.hasRemaining() )
{
break;
}
byte globalId[] = new byte[buffer.get()];
if ( buffer.limit() - buffer.position() < globalId.length )
{
break;
}
buffer.get( globalId );
Xid xid = new XidImpl( globalId, new byte[0] );
if ( recordMap.containsKey( xid ) )
{
throw new IOException( "Tx start for same xid[" + xid
+ "] found twice" );
}
List<Record> recordList = new LinkedList<Record>();
recordList.add( new Record( recordType, globalId, null,
seqNr++ ) );
recordMap.put( xid, recordList );
nextPosition += 2 + globalId.length;
}
else if ( recordType == BRANCH_ADD )
{
if ( buffer.limit() - buffer.position() < 2 )
{
break;
}
byte globalId[] = new byte[buffer.get()];
byte branchId[] = new byte[buffer.get()];
if ( buffer.limit() - buffer.position() <
globalId.length + branchId.length )
{
break;
}
buffer.get( globalId );
buffer.get( branchId );
Xid xid = new XidImpl( globalId, new byte[0] );
if ( !recordMap.containsKey( xid ) )
{
throw new IOException( "Branch[" + new String( branchId )
+ "] found for [" + xid
+ "] but no record list found in map" );
}
recordMap.get( xid ).add(
new Record( recordType, globalId, branchId, seqNr++ ) );
nextPosition += 3 + globalId.length + branchId.length;
}
else if ( recordType == MARK_COMMIT )
{
if ( !buffer.hasRemaining() )
{
break;
}
byte globalId[] = new byte[buffer.get()];
if ( buffer.limit() - buffer.position() < globalId.length )
{
break;
}
buffer.get( globalId );
Xid xid = new XidImpl( globalId, new byte[0] );
if ( !recordMap.containsKey( xid ) )
{
throw new IOException( "Commiting xid[" + xid
+ "] mark found but no record list found in map" );
}
List<Record> recordList = recordMap.get( xid );
recordList.add( new Record( recordType, globalId, null,
seqNr++ ) );
recordMap.put( xid, recordList );
nextPosition += 2 + globalId.length;
}
else if ( recordType == TX_DONE )
{
if ( !buffer.hasRemaining() )
{
break;
}
byte globalId[] = new byte[buffer.get()];
if ( buffer.limit() - buffer.position() < globalId.length )
{
break;
}
buffer.get( globalId );
Xid xid = new XidImpl( globalId, new byte[0] );
if ( !recordMap.containsKey( xid ) )
{
throw new IOException( "Commiting xid[" + xid
+ "] mark found but no record list found in map" );
}
recordMap.remove( xid );
nextPosition += 2 + globalId.length;
}
else if ( recordType == 0 )
{
continue;
}
else
{
throw new IOException( "Unknown type: " + recordType );
}
if ( (buffer.limit() - buffer.position()) <
(3 + Xid.MAXGTRIDSIZE + Xid.MAXBQUALSIZE) )
{
// make sure we don't try to read non full entry
buffer.clear();
fileChannel.position( nextPosition );
fileChannel.read( buffer );
buffer.flip();
}
}
return recordMap.values().iterator();
}
/**
* Switches log file. Copies the dangling records in current log file to the
* <CODE>newFile</CODE> and the makes the switch closing the old log file.
*
* @param newFile
* The filename of the new file to switch to
* @throws IOException
* If unable to switch log file
*/
public synchronized void switchToLogFile( String newFile )
throws IOException
{
if ( newFile == null )
{
throw new IllegalArgumentException( "Null filename" );
}
// copy all dangling records from current log to new log
force();
Iterator<List<Record>> itr = getDanglingRecords();
close();
List<Record> records = new ArrayList<Record>();
while ( itr.hasNext() )
{
records.addAll( itr.next() );
}
Collections.sort( records, new Comparator<Record>()
{
public int compare( Record r1, Record r2 )
{
return r1.getSequenceNumber() - r2.getSequenceNumber();
}
} );
Iterator<Record> recordItr = records.iterator();
fileChannel = new RandomAccessFile( newFile, "rw" ).getChannel();
fileChannel.position( fileChannel.size() );
name = newFile;
truncate();
while ( recordItr.hasNext() )
{
Record record = recordItr.next();
writeRecord( record );
}
force();
}
}