/* * Bitronix Transaction Manager * * Copyright (c) 2010, Bitronix Software. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * 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 Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package bitronix.tm.resource.common; import java.util.*; import javax.transaction.Synchronization; import javax.transaction.xa.XAResource; import org.slf4j.*; import bitronix.tm.*; import bitronix.tm.internal.*; import bitronix.tm.recovery.*; import bitronix.tm.utils.*; import bitronix.tm.utils.CryptoEngine; /** * Generic XA pool. {@link XAStatefulHolder} instances are created by the {@link XAPool} out of a * {@link XAResourceProducer}. Those objects are then pooled and can be retrieved and/or recycled by the pool * depending on the running XA transaction's and the {@link XAStatefulHolder}'s states. * * @author lorban */ public class XAPool implements StateChangeListener { private final static Logger log = LoggerFactory.getLogger(XAPool.class); private final static String PASSWORD_PROPERTY_NAME = "password"; private Map statefulHolderTransactionMap = new HashMap(); private List objects = new ArrayList(); private ResourceBean bean; private XAResourceProducer xaResourceProducer; private Object xaFactory; private boolean failed = false; public XAPool(XAResourceProducer xaResourceProducer, ResourceBean bean) throws Exception { this.xaResourceProducer = xaResourceProducer; this.bean = bean; if (bean.getMaxPoolSize() < 1 || bean.getMinPoolSize() > bean.getMaxPoolSize()) throw new IllegalArgumentException("cannot create a pool with min " + bean.getMinPoolSize() + " connection(s) and max " + bean.getMaxPoolSize() + " connection(s)"); if (bean.getAcquireIncrement() < 1) throw new IllegalArgumentException("cannot create a pool with a connection acquisition increment less than 1, configured value is " + bean.getAcquireIncrement()); xaFactory = createXAFactory(bean); init(); if (bean.getIgnoreRecoveryFailures()) log.warn("resource '" + bean.getUniqueName() + "' is configured to ignore recovery failures, make sure this setting is not enabled on a production system!"); } private void init() throws Exception { growUntilMinPoolSize(); if (bean.getMaxIdleTime() > 0) { TransactionManagerServices.getTaskScheduler().schedulePoolShrinking(this); } } private void growUntilMinPoolSize() throws Exception { for (int i = (int)totalPoolSize(); i < bean.getMinPoolSize() ;i++) { createPooledObject(xaFactory); } } public Object getXAFactory() { return xaFactory; } public synchronized void setFailed(boolean failed) { this.failed = failed; } public synchronized boolean isFailed() { return failed; } public synchronized Object getConnectionHandle() throws Exception { return getConnectionHandle(true); } public synchronized Object getConnectionHandle(boolean recycle) throws Exception { if (failed) { try { if (log.isDebugEnabled()) log.debug("resource '" + bean.getUniqueName() + "' is marked as failed, resetting and recovering it before trying connection acquisition"); close(); init(); IncrementalRecoverer.recover(xaResourceProducer); } catch (RecoveryException ex) { throw new BitronixRuntimeException("incremental recovery failed when trying to acquire a connection from failed resource '" + bean.getUniqueName() + "'", ex); } catch (Exception ex) { throw new BitronixRuntimeException("pool reset failed when trying to acquire a connection from failed resource '" + bean.getUniqueName() + "'", ex); } } long remainingTime = bean.getAcquisitionTimeout() * 1000L; long before = System.currentTimeMillis(); while (true) { XAStatefulHolder xaStatefulHolder = null; if (recycle) { if (bean.getShareTransactionConnections()) { xaStatefulHolder = getSharedXAStatefulHolder(); } else { xaStatefulHolder = getNotAccessible(); } } if (xaStatefulHolder == null) { xaStatefulHolder = getInPool(); } if (log.isDebugEnabled()) log.debug("found " + Decoder.decodeXAStatefulHolderState(xaStatefulHolder.getState()) + " connection " + xaStatefulHolder + " from " + this); try { // getConnection() here could throw an exception, if it doesn't the connection is // still alive and we can share it (if sharing is enabled) Object connectionHandle = xaStatefulHolder.getConnectionHandle(); if (bean.getShareTransactionConnections()) { putSharedXAStatefulHolder(xaStatefulHolder); } return connectionHandle; } catch (Exception ex) { if (log.isDebugEnabled()) log.debug("connection is invalid, trying to close it", ex); try { xaStatefulHolder.close(); } catch (Exception ex2) { if (log.isDebugEnabled()) log.debug("exception while trying to close invalid connection, ignoring it", ex2); } objects.remove(xaStatefulHolder); if (log.isDebugEnabled()) log.debug("removed invalid connection " + xaStatefulHolder + " from " + this); if (log.isDebugEnabled()) log.debug("waiting " + bean.getAcquisitionInterval() + "s before trying to acquire a connection again from " + this); try { wait(bean.getAcquisitionInterval() * 1000L); } catch (InterruptedException ex2) { // ignore } // check for timeout long now = System.currentTimeMillis(); remainingTime -= (now - before); if (remainingTime <= 0) { throw new BitronixRuntimeException("cannot get valid connection from " + this + " after trying for " + bean.getAcquisitionTimeout() + "s", ex); } } } // while true } public synchronized void close() { if (log.isDebugEnabled()) log.debug("closing all connections of " + this); for (int i = 0; i < totalPoolSize(); i++) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); try { // This change is unrelated to BTM-35, but suppresses noise in the unit test // output. Connections that are already in STATE_CLOSED should not be closed // again. if (xaStatefulHolder.getState() != XAStatefulHolder.STATE_CLOSED) { xaStatefulHolder.close(); } } catch (Exception ex) { if (log.isDebugEnabled()) log.debug("ignoring exception while closing connection " + xaStatefulHolder, ex); } } if (TransactionManagerServices.isTaskSchedulerRunning()) TransactionManagerServices.getTaskScheduler().cancelPoolShrinking(this); objects.clear(); failed = false; } public synchronized long totalPoolSize() { return objects.size(); } public synchronized long inPoolSize() { int count = 0; for (int i = 0; i < totalPoolSize(); i++) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); if (xaStatefulHolder.getState() == XAStatefulHolder.STATE_IN_POOL) count++; } return count; } public void stateChanged(XAStatefulHolder source, int oldState, int newState) { if (newState == XAStatefulHolder.STATE_IN_POOL) { if (log.isDebugEnabled()) log.debug("a connection's state changed to IN_POOL, notifying a thread eventually waiting for a connection"); synchronized (this) { notify(); } } } public void stateChanging(XAStatefulHolder source, int currentState, int futureState) { } public synchronized XAResourceHolder findXAResourceHolder(XAResource xaResource) { for (int i = 0; i < totalPoolSize(); i++) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); List xaResourceHolders = xaStatefulHolder.getXAResourceHolders(); for (int j = 0; j < xaResourceHolders.size(); j++) { XAResourceHolder holder = (XAResourceHolder) xaResourceHolders.get(j); if (holder.getXAResource() == xaResource) return holder; } } return null; } public List getXAResourceHolders() { return objects; } public Date getNextShrinkDate() { return new Date(System.currentTimeMillis() + bean.getMaxIdleTime() * 1000); } public synchronized void shrink() throws Exception { if (log.isDebugEnabled()) log.debug("shrinking " + this); List toRemoveXaStatefulHolders = new ArrayList(); long now = System.currentTimeMillis(); for (int i = 0; i < totalPoolSize(); i++) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); if (xaStatefulHolder.getState() != XAStatefulHolder.STATE_IN_POOL) continue; long expirationTime = (xaStatefulHolder.getLastReleaseDate().getTime() + (bean.getMaxIdleTime() * 1000)); if (log.isDebugEnabled()) log.debug("checking if connection can be closed: " + xaStatefulHolder + " - closing time: " + expirationTime + ", now time: " + now); if (expirationTime <= now) { try { xaStatefulHolder.close(); } catch (Exception ex) { log.warn("error closing " + xaStatefulHolder, ex); } toRemoveXaStatefulHolders.add(xaStatefulHolder); } } // for if (log.isDebugEnabled()) log.debug("closed " + toRemoveXaStatefulHolders.size() + " idle connection(s)"); objects.removeAll(toRemoveXaStatefulHolders); growUntilMinPoolSize(); if (log.isDebugEnabled()) log.debug("shrunk " + this); } public synchronized void reset() throws Exception { if (log.isDebugEnabled()) log.debug("resetting " + this); List toRemoveXaStatefulHolders = new ArrayList(); for (int i = 0; i < totalPoolSize(); i++) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); if (xaStatefulHolder.getState() != XAStatefulHolder.STATE_IN_POOL) continue; try { xaStatefulHolder.close(); } catch (Exception ex) { log.warn("error closing " + xaStatefulHolder, ex); } toRemoveXaStatefulHolders.add(xaStatefulHolder); } if (log.isDebugEnabled()) log.debug("closed " + toRemoveXaStatefulHolders.size() + " connection(s)"); objects.removeAll(toRemoveXaStatefulHolders); growUntilMinPoolSize(); if (log.isDebugEnabled()) log.debug("reset " + this); } public String toString() { return "an XAPool of resource " + bean.getUniqueName() + " with " + totalPoolSize() + " connection(s) (" + inPoolSize() + " still available)" + (failed ? " -failed-" : ""); } private void createPooledObject(Object xaFactory) throws Exception { XAStatefulHolder xaStatefulHolder = xaResourceProducer.createPooledConnection(xaFactory, bean); xaStatefulHolder.addStateChangeEventListener(this); objects.add(xaStatefulHolder); } private static Object createXAFactory(ResourceBean bean) throws Exception { String className = bean.getClassName(); if (className == null) throw new IllegalArgumentException("className cannot be null"); Class xaFactoryClass = ClassLoaderUtils.loadClass(className); Object xaFactory = xaFactoryClass.newInstance(); Iterator it = bean.getDriverProperties().entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); String name = (String) entry.getKey(); String value = (String) entry.getValue(); if (name.endsWith(PASSWORD_PROPERTY_NAME)) { value = decrypt(value); } if (log.isDebugEnabled()) log.debug("setting vendor property '" + name + "' to '" + value + "'"); PropertyUtils.setProperty(xaFactory, name, value); } return xaFactory; } private static String decrypt(String resourcePassword) throws Exception { int startIdx = resourcePassword.indexOf("{"); int endIdx = resourcePassword.indexOf("}"); if (startIdx != 0 || endIdx == -1) return resourcePassword; String cipher = resourcePassword.substring(1, endIdx); if (log.isDebugEnabled()) log.debug("resource password is encrypted, decrypting " + resourcePassword); return CryptoEngine.decrypt(cipher, resourcePassword.substring(endIdx + 1)); } private XAStatefulHolder getNotAccessible() { if (log.isDebugEnabled()) log.debug("trying to recycle a NOT_ACCESSIBLE connection of " + this); BitronixTransaction transaction = TransactionContextHelper.currentTransaction(); if (transaction == null) { if (log.isDebugEnabled()) log.debug("no current transaction, no connection can be in state NOT_ACCESSIBLE when there is no global transaction context"); return null; } Uid currentTxGtrid = transaction.getResourceManager().getGtrid(); if (log.isDebugEnabled()) log.debug("current transaction GTRID is [" + currentTxGtrid + "]"); for (int i = 0; i < totalPoolSize(); i++) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); if (xaStatefulHolder.getState() == XAStatefulHolder.STATE_NOT_ACCESSIBLE) { if (log.isDebugEnabled()) log.debug("found a connection in NOT_ACCESSIBLE state: " + xaStatefulHolder); if (containsXAResourceHolderMatchingGtrid(xaStatefulHolder, currentTxGtrid)) return xaStatefulHolder; } } // for if (log.isDebugEnabled()) log.debug("no NOT_ACCESSIBLE connection enlisted in this transaction"); return null; } private boolean containsXAResourceHolderMatchingGtrid(XAStatefulHolder xaStatefulHolder, Uid currentTxGtrid) { List xaResourceHolders = xaStatefulHolder.getXAResourceHolders(); if (log.isDebugEnabled()) log.debug(xaResourceHolders.size() + " xa resource(s) created by connection in NOT_ACCESSIBLE state: " + xaStatefulHolder); for (int i = 0; i < xaResourceHolders.size(); i++) { XAResourceHolder xaResourceHolder = (XAResourceHolder) xaResourceHolders.get(i); Map statesForGtrid = xaResourceHolder.getXAResourceHolderStatesForGtrid(currentTxGtrid); if (statesForGtrid == null) return false; Iterator statesForGtridIt = statesForGtrid.values().iterator(); while (statesForGtridIt.hasNext()) { XAResourceHolderState xaResourceHolderState = (XAResourceHolderState) statesForGtridIt.next(); // compare GTRIDs BitronixXid bitronixXid = xaResourceHolderState.getXid(); Uid resourceGtrid = bitronixXid.getGlobalTransactionIdUid(); if (log.isDebugEnabled()) log.debug("NOT_ACCESSIBLE xa resource GTRID: " + resourceGtrid); if (currentTxGtrid.equals(resourceGtrid)) { if (log.isDebugEnabled()) log.debug("NOT_ACCESSIBLE xa resource's GTRID matched this transaction's GTRID, recycling it"); return true; } } } return false; } private XAStatefulHolder getInPool() throws Exception { if (log.isDebugEnabled()) log.debug("getting a IN_POOL connection from " + this); if (inPoolSize() == 0) { if (log.isDebugEnabled()) log.debug("no more free connection in " + this + ", trying to grow it"); grow(); } waitForConnectionInPool(); for (int i = 0; i < totalPoolSize(); i++) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); if (xaStatefulHolder.getState() == XAStatefulHolder.STATE_IN_POOL) return xaStatefulHolder; } throw new BitronixRuntimeException("pool does not contain IN_POOL connection while it should !"); } private synchronized void grow() throws Exception { if (totalPoolSize() < bean.getMaxPoolSize()) { long increment = bean.getAcquireIncrement(); if (totalPoolSize() + increment > bean.getMaxPoolSize()) { increment = bean.getMaxPoolSize() - totalPoolSize(); } if (log.isDebugEnabled()) log.debug("incrementing " + bean.getUniqueName() + " pool size by " + increment + " unit(s) to reach " + (totalPoolSize() + increment) + " connection(s)"); for (int i=0; i < increment ;i++) { createPooledObject(xaFactory); } } else { if (log.isDebugEnabled()) log.debug("pool " + bean.getUniqueName() + " already at max size of " + totalPoolSize() + " connection(s), not growing it"); } } private synchronized void waitForConnectionInPool() { long remainingTime = bean.getAcquisitionTimeout() * 1000L; if (log.isDebugEnabled()) log.debug("waiting for IN_POOL connections count to be > 0, currently is " + inPoolSize()); while (inPoolSize() == 0) { long before = System.currentTimeMillis(); try { if (log.isDebugEnabled()) log.debug("waiting " + remainingTime + "ms"); wait(remainingTime); if (log.isDebugEnabled()) log.debug("waiting over, IN_POOL connections count is now " + inPoolSize()); } catch (InterruptedException ex) { // ignore } long now = System.currentTimeMillis(); remainingTime -= (now - before); if (remainingTime <= 0 && inPoolSize() == 0) { if (log.isDebugEnabled()) log.debug("connection pool dequeue timed out"); if (TransactionManagerServices.isTransactionManagerRunning()) TransactionManagerServices.getTransactionManager().dumpTransactionContexts(); throw new BitronixRuntimeException("XA pool of resource " + bean.getUniqueName() + " still empty after " + bean.getAcquisitionTimeout() + "s wait time"); } } // while } /** * Shared Connection Handling */ /** * Try to get a shared XAStatefulHolder. This method will either return * a shared XAStatefulHolder or <code>null</code>. If there is no current * transaction, XAStatefulHolder's are not shared. If there is a transaction * <i>and</i> there is a XAStatefulHolder associated with this thread already, * we return that XAStatefulHolder (provided it is ACCESSIBLE or NOT_ACCESSIBLE). * * @return a shared XAStatefulHolder or <code>null</code> */ private XAStatefulHolder getSharedXAStatefulHolder() { BitronixTransaction transaction = TransactionContextHelper.currentTransaction(); if (transaction == null) { if (log.isDebugEnabled()) log.debug("no current transaction, shared connection map will not be used"); return null; } Uid currentTxGtrid = transaction.getResourceManager().getGtrid(); ThreadLocal threadLocal = (ThreadLocal) statefulHolderTransactionMap.get(currentTxGtrid); if (threadLocal != null) { XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) threadLocal.get(); // Additional sanity checks... if (xaStatefulHolder != null && xaStatefulHolder.getState() != XAStatefulHolder.STATE_IN_POOL && xaStatefulHolder.getState() != XAStatefulHolder.STATE_CLOSED) { if (log.isDebugEnabled()) log.debug("sharing connection " + xaStatefulHolder + " in transaction " + currentTxGtrid); return xaStatefulHolder; } } return null; } /** * Try to share a XAStatefulHolder with other callers on this thread. If * there is no current transaction, the XAStatefulHolder is not put into the * ThreadLocal. If there is a transaction, and it is the first time we're * attempting to share a XAStatefulHolder on this thread, then we register * a Synchronization so we can pull the ThreadLocals out of the shared map * when the transaction completes (either commit() or rollback()). Without * the Synchronization we would "leak". * * @param xaStatefulHolder a XAStatefulHolder to share with other callers * on this thread. */ private void putSharedXAStatefulHolder(final XAStatefulHolder xaStatefulHolder) { BitronixTransaction transaction = TransactionContextHelper.currentTransaction(); if (transaction == null) { if (log.isDebugEnabled()) log.debug("no current transaction, not adding " + xaStatefulHolder + " to shared connection map"); return; } final Uid currentTxGtrid = transaction.getResourceManager().getGtrid(); ThreadLocal threadLocal = (ThreadLocal) statefulHolderTransactionMap.get(currentTxGtrid); if (threadLocal == null) { // This is the first time this TxGtrid/ThreadLocal is going into the map, // register interest in synchronization so we can remove it at commit/rollback try { transaction.registerSynchronization(new SharedStatefulHolderCleanupSynchronization(currentTxGtrid)); } catch (Exception e) { // OK, forget it. The transaction is either rollback only or already finished. return; } threadLocal = new ThreadLocal(); statefulHolderTransactionMap.put(currentTxGtrid, threadLocal); if (log.isDebugEnabled()) log.debug("added shared connection mapping for " + currentTxGtrid + " holder " + xaStatefulHolder); } // Set the XAStatefulHolder on the ThreadLocal. Even if we've already set it before, // it's safe -- checking would be more expensive than just setting it again. threadLocal.set(xaStatefulHolder); } private final class SharedStatefulHolderCleanupSynchronization implements Synchronization { private Uid gtrid; private SharedStatefulHolderCleanupSynchronization(Uid gtrid) { this.gtrid = gtrid; } public void beforeCompletion() { } public void afterCompletion(int status) { synchronized (XAPool.this) { statefulHolderTransactionMap.remove(gtrid); if (log.isDebugEnabled()) log.debug("deleted shared connection mappings for " + gtrid); } } public String toString() { return "a SharedStatefulHolderCleanupSynchronization with GTRID [" + gtrid + "]"; } } }