/* * 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.IOException; import java.util.ArrayList; 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 java.util.logging.Logger; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import org.neo4j.kernel.impl.util.ArrayMap; import org.neo4j.kernel.impl.util.StringLogger; // make package access? public class XaResourceManager { private final ArrayMap<XAResource,Xid> xaResourceMap = new ArrayMap<XAResource,Xid>(); private final ArrayMap<Xid,XidStatus> xidMap = new ArrayMap<Xid,XidStatus>(); private int recoveredTxCount = 0; private Set<Integer> recoveredDoneRecords = new HashSet<Integer>(); private XaLogicalLog log = null; private final XaTransactionFactory tf; private final String name; private StringLogger msgLog; XaResourceManager( XaTransactionFactory tf, String name ) { this.tf = tf; this.name = name; } synchronized void setLogicalLog( XaLogicalLog log ) { this.log = log; this.msgLog = log.getStringLogger(); } synchronized XaTransaction getXaTransaction( XAResource xaRes ) throws XAException { XidStatus status = xidMap.get( xaResourceMap.get( xaRes ) ); if ( status == null ) { throw new XAException( "Resource[" + xaRes + "] not enlisted" ); } return status.getTransactionStatus().getTransaction(); } synchronized void start( XAResource xaResource, Xid xid ) throws XAException { if ( xaResourceMap.get( xaResource ) != null ) { throw new XAException( "Resource[" + xaResource + "] already enlisted or suspended" ); } xaResourceMap.put( xaResource, xid ); if ( xidMap.get( xid ) == null ) { int identifier = log.start( xid ); XaTransaction xaTx = tf.create( identifier ); xidMap.put( xid, new XidStatus( xaTx ) ); } } synchronized void injectStart( Xid xid, XaTransaction tx ) throws IOException { if ( xidMap.get( xid ) != null ) { throw new IOException( "Inject start failed, xid: " + xid + " already injected" ); } xidMap.put( xid, new XidStatus( tx ) ); recoveredTxCount++; } synchronized void resume( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } if ( status.getActive() ) { throw new XAException( "Xid [" + xid + "] not suspended" ); } status.setActive( true ); } synchronized void join( XAResource xaResource, Xid xid ) throws XAException { if ( xidMap.get( xid ) == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } if ( xaResourceMap.get( xaResource ) != null ) { throw new XAException( "Resource[" + xaResource + "] already enlisted" ); } xaResourceMap.put( xaResource, xid ); } synchronized void end( XAResource xaResource, Xid xid ) throws XAException { Xid xidEntry = xaResourceMap.remove( xaResource ); if ( xidEntry == null ) { throw new XAException( "Resource[" + xaResource + "] not enlisted" ); } } synchronized void suspend( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } if ( !status.getActive() ) { throw new XAException( "Xid[" + xid + "] already suspended" ); } status.setActive( false ); } synchronized void fail( XAResource xaResource, Xid xid ) throws XAException { if ( xidMap.get( xid ) == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } Xid xidEntry = xaResourceMap.remove( xaResource ); if ( xidEntry == null ) { throw new XAException( "Resource[" + xaResource + "] not enlisted" ); } XidStatus status = xidMap.get( xid ); status.getTransactionStatus().markAsRollback(); } synchronized void validate( XAResource xaResource ) throws XAException { XidStatus status = xidMap.get( xaResourceMap.get( xaResource ) ); if ( status == null ) { throw new XAException( "Resource[" + xaResource + "] not enlisted" ); } if ( !status.getActive() ) { throw new XAException( "Resource[" + xaResource + "] suspended" ); } } // TODO: check so we're not currently committing on the resource synchronized void destroy( XAResource xaResource ) { xaResourceMap.remove( xaResource ); } private static class XidStatus { private boolean active = true; private TransactionStatus txStatus; XidStatus( XaTransaction xaTransaction ) { txStatus = new TransactionStatus( xaTransaction ); } void setActive( boolean active ) { this.active = active; } boolean getActive() { return this.active; } TransactionStatus getTransactionStatus() { return txStatus; } } private static class TransactionStatus { private boolean prepared = false; private boolean commitStarted = false; private boolean rollback = false; private final XaTransaction xaTransaction; TransactionStatus( XaTransaction xaTransaction ) { this.xaTransaction = xaTransaction; } void markAsPrepared() { prepared = true; } void markAsRollback() { rollback = true; } void markCommitStarted() { commitStarted = true; } boolean prepared() { return prepared; } boolean rollback() { return rollback; } boolean commitStarted() { return commitStarted; } XaTransaction getTransaction() { return xaTransaction; } public String toString() { return "TransactionStatus[" + xaTransaction.getIdentifier() + ", prepared=" + prepared + ", commitStarted=" + commitStarted + ", rolledback=" + rollback + "]"; } } synchronized int prepare( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); if ( xaTransaction.isReadOnly() ) { log.done( xaTransaction.getIdentifier() ); xidMap.remove( xid ); if ( xaTransaction.isRecovered() ) { recoveredTxCount--; checkIfRecoveryComplete(); } return XAResource.XA_RDONLY; } else { xaTransaction.prepare(); log.prepare( xaTransaction.getIdentifier() ); txStatus.markAsPrepared(); return XAResource.XA_OK; } } // called from XaResource internal recovery // returns true if read only and should be removed... synchronized boolean injectPrepare( Xid xid ) throws IOException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new IOException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); if ( xaTransaction.isReadOnly() ) { xidMap.remove( xid ); if ( xaTransaction.isRecovered() ) { recoveredTxCount--; checkIfRecoveryComplete(); } return true; } else { txOrderMap.put( xid, nextTxOrder++ ); txStatus.markAsPrepared(); return false; } } private Map<Xid,Integer> txOrderMap = new HashMap<Xid,Integer>(); private int nextTxOrder = 0; // called during recovery // if not read only transaction will be commited. synchronized void injectOnePhaseCommit( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); txOrderMap.put( xid, nextTxOrder++ ); txStatus.markAsPrepared(); txStatus.markCommitStarted(); XaTransaction xaTransaction = txStatus.getTransaction(); xaTransaction.commit(); } synchronized void injectTwoPhaseCommit( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); txOrderMap.put( xid, nextTxOrder++ ); txStatus.markAsPrepared(); txStatus.markCommitStarted(); XaTransaction xaTransaction = txStatus.getTransaction(); xaTransaction.commit(); } synchronized XaTransaction commit( Xid xid, boolean onePhase ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); if ( onePhase ) { if ( !xaTransaction.isReadOnly() ) { if ( !xaTransaction.isRecovered() ) { xaTransaction.prepare(); log.commitOnePhase( xaTransaction.getIdentifier() ); } } txStatus.markAsPrepared(); } if ( !txStatus.prepared() || txStatus.rollback() ) { throw new XAException( "Transaction not prepared or " + "(marked as) rolledbacked" ); } if ( !xaTransaction.isReadOnly() ) { if ( !xaTransaction.isRecovered() ) { if ( !onePhase ) { log.commitTwoPhase( xaTransaction.getIdentifier() ); } } txStatus.markCommitStarted(); xaTransaction.commit(); } if ( !xaTransaction.isRecovered() ) { log.done( xaTransaction.getIdentifier() ); } else if ( !log.scanIsComplete() ) { recoveredDoneRecords.add( xaTransaction.getIdentifier() ); } xidMap.remove( xid ); if ( xaTransaction.isRecovered() ) { recoveredTxCount--; checkIfRecoveryComplete(); } return xaTransaction; } synchronized XaTransaction rollback( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); if ( txStatus.commitStarted() ) { throw new XAException( "Transaction already started commit" ); } txStatus.markAsRollback(); xaTransaction.rollback(); log.done( xaTransaction.getIdentifier() ); xidMap.remove( xid ); if ( xaTransaction.isRecovered() ) { recoveredTxCount--; checkIfRecoveryComplete(); } return txStatus.getTransaction(); } synchronized XaTransaction forget( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); log.done( xaTransaction.getIdentifier() ); xidMap.remove( xid ); if ( xaTransaction.isRecovered() ) { recoveredTxCount--; checkIfRecoveryComplete(); } return xaTransaction; } synchronized void markAsRollbackOnly( Xid xid ) throws XAException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new XAException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); txStatus.markAsRollback(); } synchronized Xid[] recover( int flag ) throws XAException { List<Xid> xids = new ArrayList<Xid>(); Iterator<Xid> keyIterator = xidMap.keySet().iterator(); while ( keyIterator.hasNext() ) { xids.add( keyIterator.next() ); } return xids.toArray( new Xid[xids.size()] ); } // called from neostore internal recovery synchronized void pruneXid( Xid xid ) throws IOException { XidStatus status = xidMap.get( xid ); if ( status == null ) { throw new IOException( "Unknown xid[" + xid + "]" ); } TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); xidMap.remove( xid ); if ( xaTransaction.isRecovered() ) { recoveredDoneRecords.remove( xaTransaction.getIdentifier() ); recoveredTxCount--; checkIfRecoveryComplete(); } } synchronized void pruneXidIfExist( Xid xid ) throws IOException { XidStatus status = xidMap.get( xid ); if ( status == null ) { return; } TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); xidMap.remove( xid ); if ( xaTransaction.isRecovered() ) { recoveredTxCount--; checkIfRecoveryComplete(); } } synchronized void checkXids() throws IOException { msgLog.logMessage( "XaResourceManager[" + name + "] sorting " + xidMap.size() + " xids" ); Iterator<Xid> keyIterator = xidMap.keySet().iterator(); LinkedList<Xid> xids = new LinkedList<Xid>(); while ( keyIterator.hasNext() ) { xids.add( keyIterator.next() ); } // comparator only used here Collections.sort( xids, new Comparator<Xid>() { public int compare( Xid o1, Xid o2 ) { Integer id1 = txOrderMap.get( o1 ); Integer id2 = txOrderMap.get( o2 ); if ( id1 == null && id2 == null ) { return 0; } if ( id1 == null ) { return Integer.MAX_VALUE; } if ( id2 == null ) { return Integer.MIN_VALUE; } return id1 - id2; } } ); txOrderMap.clear(); // = null; Logger logger = Logger.getLogger( tf.getClass().getName() ); while ( !xids.isEmpty() ) { Xid xid = xids.removeFirst(); XidStatus status = xidMap.get( xid ); TransactionStatus txStatus = status.getTransactionStatus(); XaTransaction xaTransaction = txStatus.getTransaction(); int identifier = xaTransaction.getIdentifier(); if ( xaTransaction.isRecovered() ) { if ( txStatus.commitStarted() ) { logger.fine( "Marking 1PC [" + name + "] tx " + identifier + " as done" ); log.doneInternal( identifier ); xidMap.remove( xid ); recoveredTxCount--; } else if ( !txStatus.prepared() ) { logger.fine( "Rolling back non prepared tx [" + name + "]" + "txIdent[" + identifier + "]" ); log.doneInternal( xaTransaction.getIdentifier() ); xidMap.remove( xid ); recoveredTxCount--; } else { logger.fine( "2PC tx [" + name + "] " + txStatus + " txIdent[" + identifier + "]" ); } } } checkIfRecoveryComplete(); } private void checkIfRecoveryComplete() { msgLog.logMessage( "XaResourceManager[" + name + "] checkRecoveryComplete " + xidMap.size() + " xids" ); if ( log.scanIsComplete() && recoveredTxCount == 0 ) { // log.makeNewLog(); tf.recoveryComplete(); try { for ( int identifier : recoveredDoneRecords ) { log.doneInternal( identifier ); } recoveredDoneRecords.clear(); } catch ( IOException e ) { e.printStackTrace(); } msgLog.logMessage( "XaResourceManager[" + name + "] recovery completed." ); } else { msgLog.logMessage( "XaResourceManager[" + name + "] recovery not completed, xidCount=" + recoveredTxCount + " waiting for global tm" ); } } // for testing, do not use! synchronized void reset() { xaResourceMap.clear(); xidMap.clear(); log.reset(); } /** * Returns <CODE>true</CODE> if recovered transactions exist. This method * is useful to invoke after the logical log has been opened to detirmine if * there are any recovered transactions waiting for the TM to tell them what * to do. * * @return True if recovered transactions exist */ public boolean hasRecoveredTransactions() { return recoveredTxCount > 0; } }