/* * The contents of this file are subject to the Open Software License * Version 3.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.rosenlaw.com/OSL3.0.htm * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * This file is an original work developed by Netymon Pty Ltd * (http://www.netymon.com, mailto:mail@netymon.com). Portions created * by Netymon Pty Ltd are Copyright (c) 2006 Netymon Pty Ltd. * All Rights Reserved. * * Derivation from MulgaraTransactionManager Copyright (c) 2007 Topaz * Foundation under contract by Andrae Muys (mailto:andrae@netymon.com). */ package org.mulgara.resolver; // Java2 packages import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; // Third party packages import org.apache.log4j.Logger; // Local packages import org.mulgara.query.MulgaraTransactionException; /** * Manages transactions within Mulgara. * * see http://mulgara.org/confluence/display/dev/Transaction+Architecture * * Maintains association between Answer's and TransactionContext's. * Manages tracking the ownership of the write-lock. * Maintains the write-queue and any timeout algorithm desired. * Provides new/existing TransactionContext's to DatabaseSession on request. * Note: Returns new context unless Session is currently in a User Demarcated Transaction. * * @created 2006-10-06 * * @author <a href="mailto:andrae@netymon.com">Andrae Muys</a> * * @company <A href="mailto:mail@netymon.com">Netymon Pty Ltd</A> * * @copyright ©2006 <a href="http://www.netymon.com/">Netymon Pty Ltd</a> * * @licence Open Software License v3.0</a> */ public abstract class MulgaraTransactionFactory { private static final Logger logger = Logger.getLogger(MulgaraTransactionFactory.class.getName()); private static final Timer reaperTimer = new Timer("Write-lock Reaper", true); protected final MulgaraTransactionManager manager; protected final DatabaseSession session; private final Map<MulgaraTransaction, XAReaper> timeoutTasks; /** * Contains a reference the the current writing transaction IFF it is managed * by this factory. If there is no current writing transaction, or if the * writing transaction is managed by a different factory then it is null. * * Modifications of this must be holding the mutexLock. */ protected MulgaraTransaction writeTransaction; private Thread mutexHolder; private int lockCnt; protected MulgaraTransactionFactory(DatabaseSession session, MulgaraTransactionManager manager) { this.session = session; this.manager = manager; this.timeoutTasks = new HashMap<MulgaraTransaction, XAReaper>(); this.mutexHolder = null; this.lockCnt = 0; this.writeTransaction = null; } protected void transactionCreated(MulgaraTransaction transaction) { long idleTimeout = session.getIdleTimeout() > 0 ? session.getIdleTimeout() : 15*60*1000L; long txnTimeout = session.getTransactionTimeout() > 0 ? session.getTransactionTimeout() : 60*60*1000L; long now = System.currentTimeMillis(); synchronized (getMutexLock()) { timeoutTasks.put(transaction, new XAReaper(transaction, now + txnTimeout, idleTimeout, now)); } if (logger.isDebugEnabled()) { logger.debug("Timeouts set for transaction " + System.identityHashCode(transaction) + ": idleTimeout=" + idleTimeout + ", txnTimeout=" + txnTimeout); } } protected void transactionComplete(MulgaraTransaction transaction) throws MulgaraTransactionException { synchronized (getMutexLock()) { XAReaper reaper = timeoutTasks.remove(transaction); if (reaper != null) { reaper.cancel(); reaperTimer.purge(); } } } /** * Obtain a transaction context associated with a DatabaseSession. * * Either returns the existing context if: * a) we are currently within a recursive call while under implicit XA control * or * b) we are currently within an active user demarcated XA. * otherwise creates a new transaction context and associates it with the * session. */ public abstract MulgaraTransaction getTransaction(boolean write) throws MulgaraTransactionException; protected abstract Set<? extends MulgaraTransaction> getTransactions(); /** * Rollback, or abort all transactions associated with a DatabaseSession. * * Will only abort the transaction if the rollback attempt fails. */ public void closingSession() throws MulgaraTransactionException { acquireMutex(0, MulgaraTransactionException.class); try { logger.debug("Cleaning up any stale transactions on session close"); Map<MulgaraTransaction, Throwable> requiresAbort = new HashMap<MulgaraTransaction, Throwable>(); try { Throwable error = null; if (manager.isHoldingWriteLock(session)) { logger.debug("Session holds write-lock"); try { if (writeTransaction != null) { try { logger.warn("Terminating session while holding writelock:" + session + ": " + writeTransaction); writeTransaction.execute(new TransactionOperation() { public void execute() throws MulgaraTransactionException { writeTransaction.heuristicRollback("Session closed while holding write lock"); } }); } catch (Throwable th) { if (writeTransaction != null) { requiresAbort.put(writeTransaction, th); error = th; } } finally { writeTransaction = null; } } } finally { if (manager.isHoldingWriteLock(session)) // normally this will have been released manager.releaseWriteLock(session); } } else { logger.debug("Session does not hold write-lock"); } for (MulgaraTransaction transaction : new HashSet<MulgaraTransaction>(getTransactions())) { try { // This is final so we can create the closure. final MulgaraTransaction xa = transaction; transaction.execute(new TransactionOperation() { public void execute() throws MulgaraTransactionException { xa.heuristicRollback("Rollback due to session close"); } }); } catch (Throwable th) { requiresAbort.put(transaction, th); if (error == null) { error = th; } } } if (error != null) { throw new MulgaraTransactionException("Heuristic rollback failed on session close", error); } } finally { try { abortTransactions(requiresAbort); } catch (Throwable th) { try { logger.error("Error aborting transactions after heuristic rollback failure on session close", th); } catch (Throwable throw_away) { } } synchronized (getMutexLock()) { for (XAReaper reaper : timeoutTasks.values()) { reaper.cancel(); } reaperTimer.purge(); timeoutTasks.clear(); } } } finally { releaseMutex(); } } /** * Abort as many of the transactions as we can. */ protected void abortTransactions(Map<MulgaraTransaction, Throwable> requiresAbort) { try { if (!requiresAbort.isEmpty()) { // At this point the originating exception has been thrown in the caller // so we attempt to ensure it doesn't get superseeded by anything that // might be thrown here while logging any errors. try { logger.error("Heuristic Rollback Failed on session close- aborting"); } catch (Throwable throw_away) { } // Logging difficulties. try { for (MulgaraTransaction transaction : requiresAbort.keySet()) { try { transaction.abortTransaction("Heuristic Rollback failed on session close", requiresAbort.get(transaction)); } catch (Throwable th) { try { logger.error("Error aborting transaction after heuristic rollback failure on session close", th); } catch (Throwable throw_away) { } } } } catch (Throwable th) { try { logger.error("Loop error while aborting transactions after heuristic rollback failure on session close", th); } catch (Throwable throw_away) { } } } } catch (Throwable th) { try { logger.error("Unidentified error while aborting transactions after heuristic rollback failure on session close", th); } catch (Throwable throw_away) { } } } /** * Acquire the mutex. The mutex is re-entrant, but {@link #releaseMutex} must be called as many * times as this is called. * * <p>We use our own implementation here (as opposed to, say, java.util.concurrent.Lock) so we * can reliably get the current mutex-owner, and we use a lock around the mutex acquisition and * release to do atomic tests and settting of additional variables associated with the mutex. * * @param timeout how many milliseconds to wait for the mutex, or 0 to wait indefinitely * @param exc An exception class that is the type that will be thrown in case of failure. * @throws T if the mutex could not be acquired, either due to a timeout or due to an interrupt */ public final <T extends Throwable> void acquireMutex(long timeout, Class<T> exc) throws T { synchronized (getMutexLock()) { if (mutexHolder == Thread.currentThread()) { lockCnt++; return; } long deadline = System.currentTimeMillis() + timeout; while (mutexHolder != null) { long wait = deadline - System.currentTimeMillis(); if (timeout == 0) { wait = 0; } else if (wait <= 0) { throw newException(exc, "Timed out waiting to acquire lock"); } try { getMutexLock().wait(wait); } catch (InterruptedException ie) { throw newExceptionOrCause(exc, "Interrupted while waiting to acquire lock", ie); } } mutexHolder = Thread.currentThread(); lockCnt = 1; } } /** * Acquire the mutex, interrupting the existing holder if there is one. * * @param timeout how many milliseconds to wait for the mutex, or 0 to wait indefinitely * @param exc An exception class that is the type that will be thrown in case of failure. * @throws T if the mutex could not be acquired, either due to a timeout or due to an interrupt * @see #acquireMutex */ public final <T extends Throwable> void acquireMutexWithInterrupt(long timeout, Class<T> exc) throws T { synchronized (getMutexLock()) { if (mutexHolder != null && mutexHolder != Thread.currentThread()) { mutexHolder.interrupt(); } acquireMutex(timeout, exc); } } /** * Release the mutex. * * @throws IllegalStateException is the mutex is not held by the current thread */ public final void releaseMutex() { synchronized (getMutexLock()) { if (Thread.currentThread() != mutexHolder) { throw new IllegalStateException("Attempt to release mutex without holding mutex"); } assert lockCnt > 0; if (--lockCnt <= 0) { mutexHolder = null; getMutexLock().notify(); } } } /** * @return the lock used to protect access to and to implement the mutex; must be held as shortly * as possible (no blocking operations) */ public final Object getMutexLock() { return session; } /** * @return the current holder of the mutex, or null if none. Must hold the mutex-lock while * calling this. */ public final Thread getMutexHolder() { return mutexHolder; } public static <T extends Throwable> T newException(Class<T> exc, String msg) { try { return exc.getConstructor(String.class).newInstance(msg); } catch (Exception e) { throw new Error("Internal error creating " + exc, e); } } public static <T extends Throwable> T newExceptionOrCause(Class<T> exc, String msg, Throwable cause) { if (exc.isAssignableFrom(cause.getClass())) return exc.cast(cause); return exc.cast(newException(exc, msg).initCause(cause)); } private class XAReaper extends TimerTask { private final MulgaraTransaction transaction; private final long txnDeadline; private final long idleTimeout; public XAReaper(MulgaraTransaction transaction, long txnDeadline, long idleTimeout, long lastActive) { this.transaction = transaction; this.txnDeadline = txnDeadline; this.idleTimeout = idleTimeout; if (lastActive <= 0) lastActive = System.currentTimeMillis(); long nextWakeup = Math.min(txnDeadline, lastActive + idleTimeout); if (logger.isDebugEnabled()) { logger.debug("Transaction-reaper created, txn=" + transaction + ", txnDeadline=" + txnDeadline + ", idleTimeout=" + idleTimeout + ", nextWakeup=" + nextWakeup + ": " + System.identityHashCode(getMutexLock())); } reaperTimer.schedule(this, new Date(nextWakeup)); } public void run() { logger.debug("Transaction-reaper running, txn=" + transaction + ": " + System.identityHashCode(getMutexLock())); long lastActive = transaction.lastActive(); long now = System.currentTimeMillis(); synchronized (getMutexLock()) { if (timeoutTasks.remove(transaction) == null) return; // looks like we got cleaned up if (now < txnDeadline && ((lastActive <= 0) || (now < lastActive + idleTimeout))) { if (logger.isDebugEnabled()) { logger.debug("Transaction still active: " + lastActive + " time: " + now + " idle-timeout: " + idleTimeout + " - rescheduling timer"); } timeoutTasks.put(transaction, new XAReaper(transaction, txnDeadline, idleTimeout, lastActive)); return; } } final String txnType = (now >= txnDeadline) ? "over-extended" : "inactive"; final String toType = (now >= txnDeadline) ? "transaction" : "idle"; logger.warn("Rolling back " + txnType + " transaction"); new Thread(toType + "-timeout executor") { public void run() { try { transaction.heuristicRollback(toType + "-timeout"); } catch (MulgaraTransactionException em) { logger.warn("Exception thrown while rolling back " + txnType + " transaction"); } } }.start(); } } }