/* * JBoss, Home of Professional Open Source. * Copyright 2006, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file 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.jboss.resource.connectionmanager; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import javax.resource.ResourceException; import javax.resource.spi.ConnectionRequestInfo; import javax.transaction.Synchronization; import javax.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import org.jboss.jca.spi.ComponentStack; import org.jboss.system.ServiceMBeanSupport; import org.jboss.tm.TxUtils; import org.jboss.tm.usertx.UserTransactionListener; import org.jboss.tm.usertx.client.ServerVMClientUserTransaction; import org.jboss.util.Strings; /** * The CachedConnectionManager mbean manages associations between meta-aware objects * (those accessed through interceptor chains) and connection handles, and between * user transactions and connection handles. Normally there should only be one * such mbean. It is called by CachedConnectionInterceptor, UserTransaction, * and all BaseConnectionManager2 instances. * * @author <a href="mailto:d_jencks@users.sourceforge.net">David Jencks</a> * @author <a href="mailto:E.Guib@ceyoniq.com">Erwin Guib</a> * @author <a href="mailto:adrian@jboss.com">Adrian Brock</a> * @version $Revision: 81820 $ */ public class CachedConnectionManager extends ServiceMBeanSupport implements ServerVMClientUserTransaction.UserTransactionStartedListener, UserTransactionListener, ComponentStack, CachedConnectionManagerMBean { private boolean specCompliant; protected boolean trace; private boolean debug; protected boolean error; private TransactionManager tm; /** * ThreadLocal that holds current calling meta-programming aware * object, used in case someone is idiotic enough to cache a * connection between invocations.and want the spec required * behavior of it getting hooked up to an appropriate * ManagedConnection on each method invocation. */ private final ThreadLocal currentObjects = new ThreadLocal(); /** * The variable <code>objectToConnectionManagerMap</code> holds the * map of meta-aware object to set of connections it holds, used by * the idiot spec compliant behavior. */ private final Map objectToConnectionManagerMap = new HashMap(); /** * Connection stacktraces */ private Map connectionStackTraces = new WeakHashMap(); /** * Default CachedConnectionManager managed constructor for mbeans. * Remember that this mbean should be a singleton. * * @jmx.managed-constructor */ public CachedConnectionManager() { super(); trace = log.isTraceEnabled(); } public boolean isSpecCompliant() { return specCompliant; } public void setSpecCompliant(boolean specCompliant) { if (specCompliant) log.warn("THE SpecCompliant ATTRIBUTE IS MISNAMED SEE http://jira.jboss.com/jira/browse/JBAS-1662"); this.specCompliant = specCompliant; } public boolean isDebug() { return debug; } public void setDebug(boolean value) { this.debug = value; } public boolean isError() { return error; } public void setError(boolean value) { this.error = value; } public TransactionManager getTransactionManager() { return tm; } public void setTransactionManager(TransactionManager tm) { this.tm = tm; // FIXME we should be injecting onto the synchronizer directly if (tm != null) TransactionSynchronizer.setTransactionManager(tm); } public CachedConnectionManager getInstance() { return this; } public int getInUseConnections() { synchronized (connectionStackTraces) { return connectionStackTraces.size(); } } public Map listInUseConnections() { synchronized (connectionStackTraces) { HashMap result = new HashMap(); for (Iterator i = connectionStackTraces.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry) i.next(); Throwable stackTrace = (Throwable) entry.getValue(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); stackTrace.printStackTrace(ps); result.put(entry.getKey().toString(), baos.toString()); } return result; } } //Object registration for meta-aware objects (i.e. this is called by interceptors) /** * Describe <code>pushMetaAwareObject</code> method here. * * @see org.jboss.jca.spi.ComponentStack#pushMetaAwareObject(Object, Set) * @param rawKey an <code>Object</code> value * @param unsharableResources the unsharable resources * @exception ResourceException if an error occurs */ public void pushMetaAwareObject(final Object rawKey, Set unsharableResources) throws ResourceException { LinkedList stack = (LinkedList) currentObjects.get(); if (stack == null) { if (trace) log.trace("new stack for key: " + Strings.defaultToString(rawKey)); stack = new LinkedList(); currentObjects.set(stack); } // end of if () else { if (trace) log.trace("old stack for key: " + Strings.defaultToString(rawKey)); //At one time I attempted to recycle connections held over method calls. //This caused problems if the other method call started a new transaction. //To assure optimal use of connections, close them before calling out. } // end of else //check for reentrancy, reconnect if not reentrant. //wrap key to be based on == rather than equals KeyConnectionAssociation key = new KeyConnectionAssociation(rawKey); if (specCompliant && !stack.contains(key)) { reconnect(key, unsharableResources); } stack.addLast(key); } /** * Describe <code>popMetaAwareObject</code> method here. * * @see org.jboss.jca.spi.ComponentStack#popMetaAwareObject(Set) * @exception ResourceException if an error occurs */ public void popMetaAwareObject(Set unsharableResources) throws ResourceException { LinkedList stack = (LinkedList) currentObjects.get(); KeyConnectionAssociation oldKey = (KeyConnectionAssociation) stack.removeLast(); if (trace) log.trace("popped object: " + Strings.defaultToString(oldKey)); if (specCompliant) { if (!stack.contains(oldKey)) { disconnect(oldKey, unsharableResources); } // end of if () } else if (debug) { if (closeAll(oldKey.getCMToConnectionsMap()) && error) throw new ResourceException("Some connections were not closed, see the log for the allocation stacktraces"); } //At one time I attempted to recycle connections held over method calls. //This caused problems if the other method call started a new transaction. //To assure optimal use of connections, close them before calling out. } KeyConnectionAssociation peekMetaAwareObject() { LinkedList stack = (LinkedList) currentObjects.get(); if (stack == null) return null; if (!stack.isEmpty()) return (KeyConnectionAssociation) stack.getLast(); else return null; } //ConnectionRegistration -- called by ConnectionCacheListeners (normally ConnectionManagers) void registerConnection(ConnectionCacheListener cm, ConnectionListener cl, Object connection, ConnectionRequestInfo cri) { if (debug) { synchronized (connectionStackTraces) { connectionStackTraces.put(connection, new Throwable("STACKTRACE")); } } KeyConnectionAssociation key = peekMetaAwareObject(); if (trace) log.trace("registering connection from " + cm + ", connection : " + connection + ", key: " + key); if (key == null) return; //not participating properly in this management scheme. ConnectionRecord cr = new ConnectionRecord(cl, connection, cri); Map cmToConnectionsMap = key.getCMToConnectionsMap(); Collection conns = (Collection) cmToConnectionsMap.get(cm); if (conns == null) { conns = new ArrayList(); cmToConnectionsMap.put(cm, conns); } conns.add(cr); } void unregisterConnection(ConnectionCacheListener cm, Object c) { if (debug) { CloseConnectionSynchronization cas = getCloseConnectionSynchronization(false); if (cas != null) cas.remove(c); synchronized (connectionStackTraces) { connectionStackTraces.remove(c); } } KeyConnectionAssociation key = peekMetaAwareObject(); if (trace) log.trace("unregistering connection from " + cm + ", object: " + c + ", key: " + key); if (key == null) return; //not participating properly in this management scheme. Map cmToConnectionsMap = key.getCMToConnectionsMap(); Collection conns = (Collection) cmToConnectionsMap.get(cm); if (conns == null) return; // Can happen if connections are "passed" between contexts for (Iterator i = conns.iterator(); i.hasNext();) { if (((ConnectionRecord) i.next()).connection == c) { i.remove(); return; } } throw new IllegalStateException("Trying to return an unknown connection2! " + c); } //called by UserTransaction after starting a transaction public void userTransactionStarted() throws SystemException { KeyConnectionAssociation key = peekMetaAwareObject(); if (trace) log.trace("user tx started, key: " + key); if (key == null) return; //not participating properly in this management scheme. Map cmToConnectionsMap = key.getCMToConnectionsMap(); Iterator cmToConnectionsMapIterator = cmToConnectionsMap.entrySet().iterator(); while (cmToConnectionsMapIterator.hasNext()) { Map.Entry cmToConnectionsMapEntry = (Map.Entry)cmToConnectionsMapIterator.next(); ConnectionCacheListener cm = (ConnectionCacheListener) cmToConnectionsMapEntry.getKey(); Collection conns = (Collection) cmToConnectionsMapEntry.getValue(); cm.transactionStarted(conns); } } /** * The <code>reconnect</code> method gets the cmToConnectionsMap * from objectToConnectionManagerMap, copies it to the key, and * reconnects all the connections in it. * * @param key a <code>KeyConnectionAssociation</code> value * @param unsharableResources a <code>Set</code> value * @exception ResourceException if an error occurs */ private void reconnect(KeyConnectionAssociation key, Set unsharableResources) throws ResourceException { Map cmToConnectionsMap = null; synchronized (objectToConnectionManagerMap) { cmToConnectionsMap = (Map) objectToConnectionManagerMap.get(key); if (cmToConnectionsMap == null) return; } key.setCMToConnectionsMap(cmToConnectionsMap); Iterator cmToConnectionsMapIterator = cmToConnectionsMap.entrySet().iterator(); while (cmToConnectionsMapIterator.hasNext()) { Map.Entry cmToConnectionsMapEntry = (Map.Entry)cmToConnectionsMapIterator.next(); ConnectionCacheListener cm = (ConnectionCacheListener) cmToConnectionsMapEntry.getKey(); Collection conns = (Collection) cmToConnectionsMapEntry.getValue(); cm.reconnect(conns, unsharableResources); } } private void disconnect(KeyConnectionAssociation key, Set unsharableResources) throws ResourceException { Map cmToConnectionsMap = key.getCMToConnectionsMap(); if (!cmToConnectionsMap.isEmpty()) { synchronized (objectToConnectionManagerMap) { objectToConnectionManagerMap.put(key, cmToConnectionsMap); } Iterator cmToConnectionsMapIterator = cmToConnectionsMap.entrySet().iterator(); while (cmToConnectionsMapIterator.hasNext()) { Map.Entry cmToConnectionsMapEntry = (Map.Entry)cmToConnectionsMapIterator.next(); ConnectionCacheListener cm = (ConnectionCacheListener) cmToConnectionsMapEntry.getKey(); Collection conns = (Collection) cmToConnectionsMapEntry.getValue(); cm.disconnect(conns, unsharableResources); } } } private boolean closeAll(Map cmToConnectionsMap) { if (debug == false) return false; boolean unclosed = false; Collection connections = cmToConnectionsMap.values(); if (connections.size() != 0) { for (Iterator i = connections.iterator(); i.hasNext();) { Collection conns = (Collection) i.next(); for (Iterator j = conns.iterator(); j.hasNext();) { Object c = ((ConnectionRecord) j.next()).connection; CloseConnectionSynchronization cas = getCloseConnectionSynchronization(true); if (cas == null) { unclosed = true; closeConnection(c); } else cas.add(c); } } } return unclosed; } /** * Describe <code>unregisterConnectionCacheListener</code> method here. * This is a shutdown method called by a connection manager. It will remove all reference * to that connection manager from the cache, so cached connections from that manager * will never be recoverable. * Possibly this method should not exist. * * @param cm a <code>ConnectionCacheListener</code> value */ void unregisterConnectionCacheListener(ConnectionCacheListener cm) { if (trace) log.trace("unregisterConnectionCacheListener: " + cm); synchronized (objectToConnectionManagerMap) { for (Iterator i = objectToConnectionManagerMap.values().iterator(); i.hasNext();) { Map cmToConnectionsMap = (Map) i.next(); if (cmToConnectionsMap != null) cmToConnectionsMap.remove(cm); } } } /** * The class <code>KeyConnectionAssociation</code> wraps objects so they may be used in hashmaps * based on their object identity rather than equals implementation. Used for keys. */ private final static class KeyConnectionAssociation { //key private final Object o; //map of cm to list of connections for that cm. private Map cmToConnectionsMap; KeyConnectionAssociation(final Object o) { this.o = o; } public boolean equals(Object other) { return (other instanceof KeyConnectionAssociation) && o == ((KeyConnectionAssociation) other).o; } public String toString() { return Strings.defaultToString(o); } public int hashCode() { return System.identityHashCode(o); } public void setCMToConnectionsMap(Map cmToConnectionsMap) { this.cmToConnectionsMap = cmToConnectionsMap; } public Map getCMToConnectionsMap() { if (cmToConnectionsMap == null) { cmToConnectionsMap = new HashMap(); } // end of if () return cmToConnectionsMap; } } private void closeConnection(Object c) { try { Throwable e; synchronized (connectionStackTraces) { e = (Throwable) connectionStackTraces.remove(c); } Method m = c.getClass().getMethod("close", new Class[]{}); try { if (e != null) log.info("Closing a connection for you. Please close them yourself: " + c, e); else log.info("Closing a connection for you. Please close them yourself: " + c); m.invoke(c, new Object[]{}); } catch (Throwable t) { log.info("Throwable trying to close a connection for you, please close it yourself", t); } } catch (NoSuchMethodException nsme) { log.info("Could not find a close method on alleged connection objects. Please close your own connections."); } } private CloseConnectionSynchronization getCloseConnectionSynchronization(boolean createIfNotFound) { try { Transaction tx = null; if (tm != null) tx = tm.getTransaction(); if (tx != null) { TransactionSynchronizer.lock(tx); try { CloseConnectionSynchronization cas = (CloseConnectionSynchronization) TransactionSynchronizer.getCCMSynchronization(tx); if (cas == null && createIfNotFound && TxUtils.isActive(tx)) { cas = new CloseConnectionSynchronization(); TransactionSynchronizer.registerCCMSynchronization(tx, cas); } return cas; } finally { TransactionSynchronizer.unlock(tx); } } } catch (Throwable t) { log.debug("Unable to synchronize with transaction", t); } return null; } private class CloseConnectionSynchronization implements Synchronization { HashSet connections = new HashSet(); boolean closing = false; public CloseConnectionSynchronization() { } public synchronized void add(Object c) { if (closing) return; connections.add(c); } public synchronized void remove(Object c) { if (closing) return; connections.remove(c); } public void beforeCompletion() { } public void afterCompletion(int status) { synchronized (this) { closing = true; } for (Iterator i = connections.iterator(); i.hasNext();) closeConnection(i.next()); connections.clear(); // Help the GC } } }