/** * Copyright 2010 JBoss Inc * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package bitronix.tm.resource.common; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import bitronix.tm.TransactionManagerServices; import bitronix.tm.BitronixTransaction; import bitronix.tm.BitronixXid; import bitronix.tm.recovery.IncrementalRecoverer; import bitronix.tm.recovery.RecoveryException; import bitronix.tm.utils.CryptoEngine; import bitronix.tm.utils.PropertyUtils; import bitronix.tm.utils.Uid; import bitronix.tm.utils.ClassLoaderUtils; import bitronix.tm.internal.*; import javax.transaction.xa.XAResource; import java.util.*; /** * 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. * <p>© <a href="http://www.bitronix.be">Bitronix Software</a></p> * * @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 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(); } private void init() throws Exception { for (int i=0; i < bean.getMinPoolSize() ;i++) { createPooledObject(xaFactory); } if (bean.getMaxIdleTime() > 0) { TransactionManagerServices.getTaskScheduler().schedulePoolShrinking(this); } } public Object getXAFactory() { return xaFactory; } public synchronized void setFailed(boolean failed) { this.failed = 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; String stateDescription = null; if (recycle) { xaStatefulHolder = getNotAccessible(); stateDescription = "NOT_ACCESSIBLE"; } if (xaStatefulHolder == null) { xaStatefulHolder = getInPool(); stateDescription = "IN_POOL"; } if (log.isDebugEnabled()) log.debug("found " + stateDescription + " connection " + xaStatefulHolder + " from " + this); try { return xaStatefulHolder.getConnectionHandle(); } 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 { xaStatefulHolder.close(); } catch (Exception ex) { if (log.isDebugEnabled()) log.debug("ignoring catched 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++) { if (totalPoolSize() - toRemoveXaStatefulHolders.size() <= bean.getMinPoolSize()) { if (log.isDebugEnabled()) log.debug("pool reached min size"); break; } XAStatefulHolder xaStatefulHolder = (XAStatefulHolder) objects.get(i); if (xaStatefulHolder.getState() != XAStatefulHolder.STATE_IN_POOL) continue; if (xaStatefulHolder.getLastReleaseDate() == null) 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) { xaStatefulHolder.close(); toRemoveXaStatefulHolders.add(xaStatefulHolder); } } // for if (log.isDebugEnabled()) log.debug("closed " + toRemoveXaStatefulHolders.size() + " idle connection(s)"); objects.removeAll(toRemoveXaStatefulHolders); } 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); XAResourceHolderState xaResourceHolderState = xaResourceHolder.getXAResourceHolderState(); if (xaResourceHolderState != null) { // 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() throws Exception { 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) { if (log.isDebugEnabled()) log.debug("connection pool dequeue timed out"); if (TransactionManagerServices.isTransactionManagerRunning()) TransactionManagerServices.getTransactionManager().dumpTransactionContexts(); throw new Exception("XA pool of resource " + bean.getUniqueName() + " still empty after " + bean.getAcquisitionTimeout() + "s wait time"); } } // while } }