/* * JBoss, Home of Professional Open Source * Copyright 2011 Red Hat Inc. and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.transaction; import net.jcip.annotations.GuardedBy; import org.infinispan.CacheException; import org.infinispan.commands.CommandsFactory; import org.infinispan.commands.tx.RollbackCommand; import org.infinispan.commands.write.WriteCommand; import org.infinispan.config.Configuration; import org.infinispan.context.InvocationContextContainer; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.factories.annotations.Inject; import org.infinispan.factories.annotations.Start; import org.infinispan.factories.annotations.Stop; import org.infinispan.interceptors.InterceptorChain; import org.infinispan.interceptors.locking.ClusteringDependentLogic; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.CacheNotifier; import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged; import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent; import org.infinispan.reconfigurableprotocol.manager.ReconfigurableReplicationManager; import org.infinispan.remoting.rpc.RpcManager; import org.infinispan.remoting.transport.Address; import org.infinispan.transaction.gmu.CommitLog; import org.infinispan.transaction.synchronization.SyncLocalTransaction; import org.infinispan.transaction.synchronization.SynchronizationAdapter; import org.infinispan.transaction.xa.CacheTransaction; import org.infinispan.transaction.xa.GlobalTransaction; import org.infinispan.transaction.xa.TransactionFactory; import org.infinispan.util.Util; import org.infinispan.util.concurrent.ConcurrentMapFactory; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import javax.transaction.Transaction; import javax.transaction.TransactionSynchronizationRegistry; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static java.util.Collections.emptySet; import static org.infinispan.util.Util.currentMillisFromNanotime; /** * Repository for {@link RemoteTransaction} and {@link org.infinispan.transaction.xa.TransactionXaAdapter}s (locally * originated transactions). * * @author Mircea.Markus@jboss.com * @author Galder ZamarreƱo * @author Pedro Ruivo * @author Sebastiano Peluso * @since 4.0 */ @Listener(sync = false) public class TransactionTable { public static final int CACHE_STOPPED_VIEW_ID = -1; private static final Log log = LogFactory.getLog(TransactionTable.class); private ConcurrentMap<Transaction, LocalTransaction> localTransactions; private ConcurrentMap<GlobalTransaction, RemoteTransaction> remoteTransactions; private final StaleTransactionCleanupService cleanupService = new StaleTransactionCleanupService(this); protected Configuration configuration; protected InvocationContextContainer icc; protected TransactionCoordinator txCoordinator; protected TransactionFactory txFactory; protected RpcManager rpcManager; protected CommandsFactory commandsFactory; private InterceptorChain invoker; private CacheNotifier notifier; private EmbeddedCacheManager cm; private TransactionSynchronizationRegistry transactionSynchronizationRegistry; protected ClusteringDependentLogic clusteringLogic; protected boolean clustered = false; private Lock minViewRecalculationLock; private ReconfigurableReplicationManager reconfigurableReplicationManager; /** * minTxViewId is the minimum view ID across all ongoing local and remote transactions. It doesn't update on * transaction creation, but only on removal. That's because it is not possible for a newly created transaction to * have an bigger view ID than the current one. */ private volatile int minTxViewId = CACHE_STOPPED_VIEW_ID; private volatile int currentViewId = CACHE_STOPPED_VIEW_ID; protected CommitLog commitLog; @Inject public void initialize(RpcManager rpcManager, Configuration configuration, InvocationContextContainer icc, InterceptorChain invoker, CacheNotifier notifier, TransactionFactory gtf, EmbeddedCacheManager cm, TransactionCoordinator txCoordinator, TransactionSynchronizationRegistry transactionSynchronizationRegistry, CommandsFactory commandsFactory, ClusteringDependentLogic clusteringDependentLogic, CommitLog commitLog, ReconfigurableReplicationManager reconfigurableReplicationManager) { this.rpcManager = rpcManager; this.configuration = configuration; this.icc = icc; this.invoker = invoker; this.notifier = notifier; this.txFactory = gtf; this.cm = cm; this.txCoordinator = txCoordinator; this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; this.commandsFactory = commandsFactory; this.clusteringLogic = clusteringDependentLogic; this.commitLog = commitLog; this.reconfigurableReplicationManager = reconfigurableReplicationManager; } @Start private void start() { final int concurrencyLevel = configuration.getConcurrencyLevel(); localTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); if (configuration.getCacheMode().isClustered()) { minViewRecalculationLock = new ReentrantLock(); // Only initialize this if we are clustered. remoteTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); cleanupService.start(configuration, rpcManager, invoker, reconfigurableReplicationManager); cm.addListener(cleanupService); cm.addListener(this); notifier.addListener(cleanupService); minTxViewId = rpcManager.getTransport().getViewId(); currentViewId = minTxViewId; log.debugf("Min view id set to %s", minTxViewId); clustered = true; } } @Stop private void stop() { if (clustered) { notifier.removeListener(cleanupService); cm.removeListener(cleanupService); cleanupService.stop(); cm.removeListener(this); currentViewId = CACHE_STOPPED_VIEW_ID; // indicate that the cache has stopped } shutDownGracefully(); } public Set<Object> getLockedKeysForRemoteTransaction(GlobalTransaction gtx) { RemoteTransaction transaction = remoteTransactions.get(gtx); if (transaction == null) return emptySet(); return transaction.getLockedKeys(); } public void remoteTransactionPrepared(GlobalTransaction gtx) { //do nothing } public void localTransactionPrepared(LocalTransaction localTransaction) { //nothing, only used by recovery } public void enlist(Transaction transaction, LocalTransaction localTransaction) { if (!localTransaction.isEnlisted()) { SynchronizationAdapter sync = new SynchronizationAdapter(localTransaction, txCoordinator, commandsFactory, rpcManager, this, clusteringLogic, configuration); if (transactionSynchronizationRegistry != null) { try { transactionSynchronizationRegistry.registerInterposedSynchronization(sync); } catch (Exception e) { log.failedSynchronizationRegistration(e); throw new CacheException(e); } } else { try { transaction.registerSynchronization(sync); } catch (Exception e) { log.failedSynchronizationRegistration(e); throw new CacheException(e); } } commitLog.initLocalTransaction(localTransaction); ((SyncLocalTransaction) localTransaction).setEnlisted(true); } } public void failureCompletingTransaction(Transaction tx) { final LocalTransaction localTransaction = localTransactions.get(tx); if (localTransaction != null) { removeLocalTransaction(localTransaction); } } /** * Returns true if the given transaction is already registered with the transaction table. * * @param tx if null false is returned */ public boolean containsLocalTx(Transaction tx) { return tx != null && localTransactions.containsKey(tx); } public int getMinViewId() { return minTxViewId; } protected void updateStateOnNodesLeaving(Collection<Address> leavers) { Set<GlobalTransaction> toKill = new HashSet<GlobalTransaction>(); for (GlobalTransaction gt : remoteTransactions.keySet()) { if (leavers.contains(gt.getAddress())) toKill.add(gt); } if (toKill.isEmpty()) log.tracef("No global transactions pertain to originator(s) %s who have left the cluster.", leavers); else log.tracef("%s global transactions pertain to leavers list %s and need to be killed", toKill.size(), leavers); for (GlobalTransaction gtx : toKill) { log.tracef("Killing remote transaction originating on leaver %s", gtx); RollbackCommand rc = new RollbackCommand(configuration.getName(), gtx); rc.init(invoker, icc, TransactionTable.this, configuration, reconfigurableReplicationManager); try { rc.perform(null); log.tracef("Rollback of transaction %s complete.", gtx); } catch (Throwable e) { log.unableToRollbackGlobalTx(gtx, e); } } log.trace("Completed cleaning transactions originating on leavers"); } /** * Returns the {@link RemoteTransaction} associated with the supplied transaction id. Returns null if no such * association exists. */ public RemoteTransaction getRemoteTransaction(GlobalTransaction txId) { return remoteTransactions.get(txId); } public void remoteTransactionRollback(GlobalTransaction gtx) { final RemoteTransaction remove = removeRemoteTransaction(gtx); log.tracef("Removed remote transaction %s? %b", gtx, remove); } /** * Creates and register a {@link RemoteTransaction} with no modifications. Returns the created transaction. * * @throws IllegalStateException if an attempt to create a {@link RemoteTransaction} for an already registered id is * made. */ public RemoteTransaction createRemoteTransaction(GlobalTransaction globalTx, WriteCommand[] modifications) { RemoteTransaction remoteTransaction = modifications == null ? txFactory.newRemoteTransaction(globalTx, currentViewId) : txFactory.newRemoteTransaction(modifications, globalTx, currentViewId); registerRemoteTransaction(globalTx, remoteTransaction); return remoteTransaction; } private void registerRemoteTransaction(GlobalTransaction gtx, RemoteTransaction rtx) { RemoteTransaction transaction = remoteTransactions.put(gtx, rtx); if (transaction != null) { log.remoteTxAlreadyRegistered(); throw new IllegalStateException("A remote transaction with the given id was already registered!!!"); } log.tracef("Created and registered remote transaction %s", rtx); } /** * Returns the {@link org.infinispan.transaction.xa.TransactionXaAdapter} corresponding to the supplied transaction. * If none exists, will be created first. */ public LocalTransaction getOrCreateLocalTransaction(Transaction transaction, TxInvocationContext ctx) { LocalTransaction current = localTransactions.get(transaction); if (current == null) { Address localAddress = rpcManager != null ? rpcManager.getTransport().getAddress() : null; GlobalTransaction tx = txFactory.newGlobalTransaction(localAddress, false); tx.setProtocolId(ctx.getProtocolId()); current = txFactory.newLocalTransaction(transaction, tx, ctx.isImplicitTransaction(), currentViewId); log.tracef("Created a new local transaction: %s", current); localTransactions.put(transaction, current); notifier.notifyTransactionRegistered(tx, ctx); } return current; } /** * Removes the {@link org.infinispan.transaction.xa.TransactionXaAdapter} corresponding to the given tx. Returns true * if such an tx exists. */ public boolean removeLocalTransaction(LocalTransaction localTransaction) { if (localTransaction != null) { reconfigurableReplicationManager.notifyLocalTransactionFinished(localTransaction.getGlobalTransaction()); } return localTransaction != null && (removeLocalTransactionInternal(localTransaction.getTransaction()) != null); } public LocalTransaction removeLocalTransaction(Transaction tx) { return removeLocalTransactionInternal(tx); } protected final LocalTransaction removeLocalTransactionInternal(Transaction tx) { LocalTransaction removed; removed = localTransactions.remove(tx); releaseResources(removed); return removed; } private void releaseResources(CacheTransaction cacheTransaction) { if (cacheTransaction != null) { if (clustered) { recalculateMinViewIdIfNeeded(cacheTransaction); } log.tracef("Removed %s from transaction table.", cacheTransaction); cacheTransaction.notifyOnTransactionFinished(); } } /** * Removes the {@link RemoteTransaction} corresponding to the given tx. */ public void remoteTransactionCommitted(GlobalTransaction gtx) { boolean totalOrder = gtx.getReconfigurableProtocol().useTotalOrder(); if (configuration.isSecondPhaseAsync() || totalOrder) { removeRemoteTransaction(gtx); log.tracef("Remote transaction removed %s", gtx); } } public final RemoteTransaction removeRemoteTransaction(GlobalTransaction txId) { reconfigurableReplicationManager.notifyRemoteTransactionFinished(txId); RemoteTransaction removed; removed = remoteTransactions.remove(txId); releaseResources(removed); return removed; } public int getRemoteTxCount() { return remoteTransactions.size(); } public int getLocalTxCount() { return localTransactions.size(); } public LocalTransaction getLocalTransaction(Transaction tx) { return localTransactions.get(tx); } public boolean containRemoteTx(GlobalTransaction globalTransaction) { return remoteTransactions.containsKey(globalTransaction); } public Collection<RemoteTransaction> getRemoteTransactions() { return remoteTransactions.values(); } protected final LocalTransaction getLocalTx(Transaction tx) { return localTransactions.get(tx); } public final Collection<LocalTransaction> getLocalTransactions() { return localTransactions.values(); } protected final void recalculateMinViewIdIfNeeded(CacheTransaction removedTransaction) { if (removedTransaction == null) throw new IllegalArgumentException("Transaction cannot be null!"); if (currentViewId != CACHE_STOPPED_VIEW_ID) { // Assume that we only get here if we are clustered. int removedTransactionViewId = removedTransaction.getViewId(); if (removedTransactionViewId < minTxViewId) { log.tracef("A transaction has a view ID (%s) that is smaller than the smallest transaction view ID (%s) this node knows about! This can happen if a concurrent thread recalculates the minimum view ID after the current transaction has been removed from the transaction table.", removedTransactionViewId, minTxViewId); } else if (removedTransactionViewId == minTxViewId && removedTransactionViewId < currentViewId) { // We should only need to re-calculate the minimum view ID if the transaction being completed // has the same ID as the smallest known transaction ID, to check what the new smallest is, and this is // not the current view ID. calculateMinViewId(removedTransactionViewId); } } } @ViewChanged public void recalculateMinViewIdOnTopologyChange(ViewChangedEvent vce) { // don't do anything if this cache is not clustered - view changes are global if (clustered) { log.debugf("View changed, recalculating minViewId"); currentViewId = vce.getViewId(); calculateMinViewId(-1); } } /** * This method calculates the minimum view ID known by the current node. This method is only used in a clustered * cache, and only invoked when either a view change is detected, or a transaction whose view ID is not the same as * the current view ID. * <p/> * This method is guarded by minViewRecalculationLock to prevent concurrent updates to the minimum view ID field. * * @param idOfRemovedTransaction the view ID associated with the transaction that triggered this recalculation, or -1 * if triggered by a view change event. */ @GuardedBy("minViewRecalculationLock") private void calculateMinViewId(int idOfRemovedTransaction) { minViewRecalculationLock.lock(); try { // We should only need to re-calculate the minimum view ID if the transaction being completed // has the same ID as the smallest known transaction ID, to check what the new smallest is. We do this check // again here, since this is now within a synchronized method. if (idOfRemovedTransaction == -1 || (idOfRemovedTransaction == minTxViewId && idOfRemovedTransaction < currentViewId)) { int minViewIdFound = currentViewId; for (CacheTransaction ct : localTransactions.values()) { int viewId = ct.getViewId(); if (viewId < minViewIdFound) minViewIdFound = viewId; } for (CacheTransaction ct : remoteTransactions.values()) { int viewId = ct.getViewId(); if (viewId < minViewIdFound) minViewIdFound = viewId; } if (minViewIdFound > minTxViewId) { log.tracef("Changing minimum view ID from %s to %s", minTxViewId, minViewIdFound); minTxViewId = minViewIdFound; } else { log.tracef("Minimum view ID still is %s; nothing to change", minViewIdFound); } } } finally { minViewRecalculationLock.unlock(); } } private boolean areTxsOnGoing() { return !localTransactions.isEmpty() || (remoteTransactions != null && !remoteTransactions.isEmpty()); } private void shutDownGracefully() { if (log.isDebugEnabled()) log.debugf("Wait for on-going transactions to finish for %s.", Util.prettyPrintTime(configuration.getCacheStopTimeout(), TimeUnit.MILLISECONDS)); long failTime = currentMillisFromNanotime() + configuration.getCacheStopTimeout(); boolean txsOnGoing = areTxsOnGoing(); while (txsOnGoing && currentMillisFromNanotime() < failTime) { try { Thread.sleep(30); txsOnGoing = areTxsOnGoing(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); if (clustered) { log.debugf("Interrupted waiting for on-going transactions to finish. %s local transactions and %s remote transactions", localTransactions.size(), remoteTransactions.size()); } else { log.debugf("Interrupted waiting for %s on-going transactions to finish.", localTransactions.size()); } } } if (txsOnGoing) { log.unfinishedTransactionsRemain(localTransactions == null ? 0 : localTransactions.size(), remoteTransactions == null ? 0 : remoteTransactions.size()); log.debugf("Local active transactions: %s, Remote active transactions %s", localTransactions, remoteTransactions); } else { log.debug("All transactions terminated"); } } /** * This method is used when Total Order protocol is enabled. The commit/rollback command can be deliver before the * prepare command. In total order, it allows the processing the commit/rollback without the remote transaction and * this can originate a race condition to create a remote transaction. * * This method creates only one transaction per global transaction, resolving the race condition * * @param globalTransaction the global transaction * @return the remote transaction */ public RemoteTransaction getOrCreateIfAbsentRemoteTransaction(GlobalTransaction globalTransaction) { RemoteTransaction remoteTransaction = remoteTransactions.get(globalTransaction); if (remoteTransaction == null) { remoteTransaction = txFactory.newRemoteTransaction(globalTransaction, currentViewId); //try to register RemoteTransaction existing = remoteTransactions.putIfAbsent(globalTransaction, remoteTransaction); if (existing != null) { log.tracef("Returning a concurrent created remote transaction %s for global transaction %s", existing, globalTransaction.prettyPrint()); remoteTransaction = existing; } else { log.tracef("Created a new remote transaction %s for global transaction %s", remoteTransaction, globalTransaction.prettyPrint()); } } else { log.tracef("Returning a existing remote transaction %s for global transaction %s", remoteTransaction, globalTransaction.prettyPrint()); } return remoteTransaction; } }