All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.kernel.impl.transaction.TxLog Maven / Gradle / Ivy

/**
 * 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 .
 */
package org.neo4j.kernel.impl.transaction;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.transaction.xa.Xid;

import org.neo4j.helpers.UTF8;
import org.neo4j.kernel.impl.nioneo.store.FileSystemAbstraction;
import org.neo4j.kernel.impl.transaction.xaframework.DirectMappedLogBuffer;
import org.neo4j.kernel.impl.transaction.xaframework.ForceMode;
import org.neo4j.kernel.impl.transaction.xaframework.LogBuffer;

// 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.
 * 

* The {@link TxManager} uses this class to keep a transaction log for * transaction recovery. *

*/ public class TxLog { private File name = null; private LogBuffer logBuffer; private int recordCount = 0; private final Set activeTransactions = new HashSet(); 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; private final FileSystemAbstraction fileSystem; private static final class ByteArrayKey { private final byte[] bytes; private ByteArrayKey( byte[] bytes ) { this.bytes = bytes; } @Override public int hashCode() { return Arrays.hashCode( bytes ); } @Override public boolean equals( Object obj ) { return obj != null && Arrays.equals( bytes, ((ByteArrayKey)obj).bytes ); } } /** * Initializes a transaction log using filename. 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 * @param msgLog * @throws IOException * If unable to open file */ public TxLog( File fileName, FileSystemAbstraction fileSystem ) throws IOException { if ( fileName == null ) { throw new IllegalArgumentException( "Null filename" ); } this.fileSystem = fileSystem; FileChannel fileChannel = fileSystem.open( fileName, "rw" ); fileChannel.position( fileChannel.size() ); logBuffer = new DirectMappedLogBuffer( fileChannel ); this.name = fileName; recreateActiveTransactionState(); } private void recreateActiveTransactionState() throws IOException { for ( List tx : getDanglingRecords() ) for ( Record record : tx ) if ( record.getType() == TX_START ) activeTransactions.add( new ByteArrayKey( record.getGlobalId() ) ); } /** * Returns the name of the transaction log. */ public String getName() { return name.getPath(); } /** * 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 synchronized void close() throws IOException { logBuffer.force(); logBuffer.getFileChannel().close(); } /** * Forces the log file (with metadata). Useful when switching log. */ public void force() throws IOException { logBuffer.force(); } /** * Truncates the file to zero size and sets the record count to zero. */ public synchronized void truncate() throws IOException { FileChannel fileChannel = logBuffer.getFileChannel(); fileChannel.position( 0 ); fileChannel.truncate( 0 ); recordCount = 0; logBuffer = new DirectMappedLogBuffer( fileChannel ); activeTransactions.clear(); } /** * Writes a TX_START 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 { assertNotNull( globalId, "global id" ); if ( !activeTransactions.add( new ByteArrayKey( globalId ) ) ) throw new IllegalStateException( "Global ID " + Arrays.toString( globalId ) + " already started" ); logBuffer.put( TX_START ).put( (byte) globalId.length ).put( globalId ); recordCount++; } private void assertNotNull( Object globalId, String name ) { if ( globalId == null ) { throw new IllegalArgumentException( "Null " + name ); } } /** * Writes a BRANCH_ADD 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 { assertNotNull( globalId, "global id" ); assertNotNull( branchId, "branch id" ); assertActive( globalId ); logBuffer.put( BRANCH_ADD ).put( (byte) globalId.length ).put( (byte) branchId.length ).put( globalId ).put( branchId ); recordCount++; } private void assertActive( byte[] globalId ) { if ( !activeTransactions.contains( new ByteArrayKey( globalId ) ) ) throw new IllegalStateException( "Global ID " + Arrays.toString( globalId ) + " not active" ); } /** * Writes a MARK_COMMIT 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[], ForceMode forceMode ) throws IOException { assertNotNull( globalId, "global id" ); assertActive( globalId ); logBuffer.put( MARK_COMMIT ).put( (byte) globalId.length ).put( globalId ); forceMode.force( logBuffer ); recordCount++; } /** * Writes a TX_DONE 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 { assertNotNull( globalId, "global id" ); if ( !activeTransactions.remove( new ByteArrayKey( globalId ) ) ) throw new IllegalStateException( "Global ID " + Arrays.toString( globalId ) + " not active" ); logBuffer.put( TX_DONE ).put( (byte) globalId.length ).put( globalId ); recordCount++; } /** * Made public for testing only. *

* 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[" + typeName() + "," + xid + "," + seqNr + "," + (1+sizeOf( globalId )+sizeOf( branchId )) + "]"; } private int sizeOf( byte[] id ) { // If id is null it means this record type doesn't have it. TX_START/MARK_COMMIT/TX_DONE // only has the global id, whereas BRANCH_ADD has got both the global and branch ids. if ( id == null ) return 0; // The length of the array (1 byte) + the actual array return 1+id.length; } String typeName() { switch ( type ) { case TX_START: return "TX_START"; case BRANCH_ADD: return "BRANCH_ADD"; case MARK_COMMIT: return "MARK_COMMIT"; case TX_DONE: return "TX_DONE"; default: return ""; } } } void writeRecord( Record record, ForceMode forceMode ) 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(), forceMode ); } 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 Iterable> getDanglingRecords() throws IOException { FileChannel fileChannel = logBuffer.getFileChannel(); ByteBuffer buffer = ByteBuffer .allocateDirect( (3 + Xid.MAXGTRIDSIZE + Xid.MAXBQUALSIZE) * 1000 ); fileChannel.position( 0 ); buffer.clear(); fileChannel.read( buffer ); buffer.flip(); // next record position long nextPosition = 0; // holds possible dangling records int seqNr = 0; Map> recordMap = new HashMap>(); 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 recordList = new LinkedList(); 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[" + UTF8.decode( 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( "Committing xid[" + xid + "] mark found but no record list found in map" ); } List 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( "Committing 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(); } /** * Switches log file. Copies the dangling records in current log file to the * newFile and then 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( File newFile ) throws IOException { if ( newFile == null ) { throw new IllegalArgumentException( "Null filename" ); } // copy all dangling records from current log to new log force(); Iterable> itr = getDanglingRecords(); close(); List records = new ArrayList(); for ( List tx : itr ) { records.addAll( tx ); } Collections.sort( records, new Comparator() { public int compare( Record r1, Record r2 ) { return r1.getSequenceNumber() - r2.getSequenceNumber(); } } ); Iterator recordItr = records.iterator(); FileChannel fileChannel = fileSystem.open( newFile, "rw" ); fileChannel.position( fileChannel.size() ); logBuffer = new DirectMappedLogBuffer( fileChannel ); name = newFile; truncate(); while ( recordItr.hasNext() ) { Record record = recordItr.next(); writeRecord( record, ForceMode.forced ); } force(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy