/*
* 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.jdbc;
import bitronix.tm.internal.BitronixRollbackSystemException;
import bitronix.tm.internal.BitronixSystemException;
import bitronix.tm.resource.common.*;
import bitronix.tm.resource.jdbc.lrc.LrcXADataSource;
import bitronix.tm.utils.Decoder;
import bitronix.tm.utils.ManagementRegistrar;
import bitronix.tm.utils.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.XAConnection;
import javax.transaction.SystemException;
import javax.transaction.xa.XAResource;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.*;
import java.util.Date;
/**
* Implementation of a JDBC pooled connection wrapping vendor's {@link XAConnection} implementation.
*
* @author lorban, brettw
*/
public class JdbcPooledConnection extends AbstractXAResourceHolder implements StateChangeListener, JdbcPooledConnectionMBean {
private final static Logger log = LoggerFactory.getLogger(JdbcPooledConnection.class);
private final static int DETECTION_TIMEOUT = 5; // seconds
private XAConnection xaConnection;
private Connection connection;
private XAResource xaResource;
private PoolingDataSource poolingDataSource;
private LruStatementCache statementsCache;
private List uncachedStatements;
private int usageCount;
/* management */
private String jmxName;
private Date acquisitionDate;
private Date lastReleaseDate;
private int jdbcVersionDetected;
private Method isValidMethod;
public JdbcPooledConnection(PoolingDataSource poolingDataSource, XAConnection xaConnection) throws SQLException {
this.poolingDataSource = poolingDataSource;
this.xaConnection = xaConnection;
this.xaResource = xaConnection.getXAResource();
this.statementsCache = new LruStatementCache(poolingDataSource.getPreparedStatementCacheSize());
this.uncachedStatements = Collections.synchronizedList(new ArrayList());
this.lastReleaseDate = new Date();
statementsCache.addEvictionListener(new LruEvictionListener() {
public void onEviction(Object value) {
PreparedStatement stmt = (PreparedStatement) value;
try {
stmt.close();
} catch (SQLException ex) {
log.warn("error closing evicted statement", ex);
}
}
});
connection = xaConnection.getConnection();
detectJdbcVersion(connection);
addStateChangeEventListener(this);
if (poolingDataSource.getClassName().equals(LrcXADataSource.class.getName())) {
if (log.isDebugEnabled()) log.debug("emulating XA for resource " + poolingDataSource.getUniqueName() + " - changing twoPcOrderingPosition to ALWAYS_LAST_POSITION");
poolingDataSource.setTwoPcOrderingPosition(Scheduler.ALWAYS_LAST_POSITION);
if (log.isDebugEnabled()) log.debug("emulating XA for resource " + poolingDataSource.getUniqueName() + " - changing deferConnectionRelease to true");
poolingDataSource.setDeferConnectionRelease(true);
if (log.isDebugEnabled()) log.debug("emulating XA for resource " + poolingDataSource.getUniqueName() + " - changing useTmJoin to true");
poolingDataSource.setUseTmJoin(true);
}
this.jmxName = "bitronix.tm:type=JDBC,UniqueName=" + ManagementRegistrar.makeValidName(poolingDataSource.getUniqueName()) + ",Id=" + poolingDataSource.incCreatedResourcesCounter();
ManagementRegistrar.register(jmxName, this);
}
private synchronized void detectJdbcVersion(Connection connection) {
if (jdbcVersionDetected > 0)
return;
try {
isValidMethod = connection.getClass().getMethod("isValid", new Class[]{Integer.TYPE});
isValidMethod.invoke(connection, new Object[] {new Integer(DETECTION_TIMEOUT)}); // test invoke
jdbcVersionDetected = 4;
if (!poolingDataSource.isEnableJdbc4ConnectionTest()) {
if (log.isDebugEnabled()) log.debug("dataSource is JDBC4 or newer and supports isValid(), but enableJdbc4ConnectionTest is not set or is false");
}
} catch (Exception ex) {
jdbcVersionDetected = 3;
} catch (AbstractMethodError er) {
// this happens if the driver implements JDBC 3 but runs on JDK 1.6+ (which embeds the JDBC 4 interfaces)
jdbcVersionDetected = 3;
}
if (log.isDebugEnabled()) log.debug("detected JDBC connection class '" + connection.getClass() + "' is version " + jdbcVersionDetected + " type");
}
private void applyIsolationLevel() throws SQLException {
String isolationLevel = getPoolingDataSource().getIsolationLevel();
if (isolationLevel != null) {
int level = translateIsolationLevel(isolationLevel);
if (level < 0) {
log.warn("invalid transaction isolation level '" + isolationLevel + "' configured, keeping the default isolation level.");
}
else {
if (log.isDebugEnabled()) log.debug("setting connection's isolation level to " + isolationLevel);
connection.setTransactionIsolation(level);
}
}
}
private static int translateIsolationLevel(String isolationLevelGuarantee) {
if ("READ_COMMITTED".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_READ_COMMITTED;
if ("READ_UNCOMMITTED".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_READ_UNCOMMITTED;
if ("REPEATABLE_READ".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_REPEATABLE_READ;
if ("SERIALIZABLE".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_SERIALIZABLE;
return -1;
}
public void close() throws SQLException {
// this should never happen, should we throw an exception or log at warn/error?
if (usageCount > 0) {
if (log.isDebugEnabled()) log.debug("close connection with usage count > 0, " + this);
}
setState(STATE_CLOSED);
// cleanup of pooled resources
statementsCache.clear();
ManagementRegistrar.unregister(jmxName);
connection.close();
xaConnection.close();
}
public RecoveryXAResourceHolder createRecoveryXAResourceHolder() {
return new RecoveryXAResourceHolder(this);
}
private void testConnection(Connection connection) throws SQLException {
if (poolingDataSource.isEnableJdbc4ConnectionTest() && jdbcVersionDetected >= 4) {
Boolean isValid = null;
try {
if (log.isDebugEnabled()) log.debug("testing with JDBC4 isValid() method, connection of " + this);
isValid = (Boolean) isValidMethod.invoke(connection, new Object[]{new Integer(poolingDataSource.getAcquisitionTimeout())});
} catch (Exception e) {
log.warn("dysfunctional JDBC4 Connection.isValid() method, or negative acquisition timeout, in call to test connection of " + this + ". Falling back to test query.");
jdbcVersionDetected = 3;
}
// if isValid is null, and exception was caught above and we fall through to the query test
if (isValid != null) {
if (isValid.booleanValue()) {
if (log.isDebugEnabled()) log.debug("isValid successfully tested connection of " + this);
return;
}
throw new SQLException("connection is no longer valid");
}
}
String query = poolingDataSource.getTestQuery();
if (query == null) {
if (log.isDebugEnabled()) log.debug("no query to test connection of " + this + ", skipping test");
return;
}
// Throws a SQLException if the connection is dead
if (log.isDebugEnabled()) log.debug("testing with query '" + query + "' connection of " + this);
PreparedStatement stmt = connection.prepareStatement(query);
ResultSet rs = stmt.executeQuery();
rs.close();
stmt.close();
if (log.isDebugEnabled()) log.debug("testQuery successfully tested connection of " + this);
}
protected void release() throws SQLException {
if (log.isDebugEnabled()) log.debug("releasing to pool " + this);
--usageCount;
// delisting
try {
TransactionContextHelper.delistFromCurrentTransaction(this);
}
catch (BitronixRollbackSystemException ex) {
throw (SQLException) new SQLException("unilateral rollback of " + this).initCause(ex);
}
catch (SystemException ex) {
throw (SQLException) new SQLException("error delisting " + this).initCause(ex);
}
finally {
// Only requeue a connection if it is no longer in use. In the case of non-shared connections,
// usageCount will always be 0 here, so the default behavior is unchanged.
if (usageCount == 0) {
try {
TransactionContextHelper.requeue(this, poolingDataSource);
} catch (BitronixSystemException ex) {
// Requeue failed, restore the usageCount to previous value (see testcase
// NewJdbcStrangeUsageMockTest.testClosingSuspendedConnectionsInDifferentContext).
// This can happen when a close is attempted while the connection is participating
// in a global transaction.
usageCount++;
// this may hide the exception thrown by delistFromCurrentTransaction() but
// an error requeuing must absolutely be reported as an exception.
// Too bad if this happens... See DualSessionWrapper.close() as well.
throw (SQLException) new SQLException("error requeuing " + this).initCause(ex);
}
if (log.isDebugEnabled()) log.debug("released to pool " + this);
}
else {
if (log.isDebugEnabled()) log.debug("not releasing " + this + " to pool yet, connection is still shared");
}
} // finally
}
public XAResource getXAResource() {
return xaResource;
}
public ResourceBean getResourceBean() {
return getPoolingDataSource();
}
public PoolingDataSource getPoolingDataSource() {
return poolingDataSource;
}
public List getXAResourceHolders() {
List xaResourceHolders = new ArrayList();
xaResourceHolders.add(this);
return xaResourceHolders;
}
public Object getConnectionHandle() throws Exception {
if (log.isDebugEnabled()) log.debug("getting connection handle from " + this);
int oldState = getState();
// Increment the usage count
usageCount++;
// Only transition to STATE_ACCESSIBLE on the first usage. If we're not sharing
// connections (default behavior) usageCount is always 1 here, so this transition
// will always occur (current behavior unchanged). If we _are_ sharing connections,
// and this is _not_ the first usage, it is valid for the state to already be
// STATE_ACCESSIBLE. Calling setState() with STATE_ACCESSIBLE when the state is
// already STATE_ACCESSIBLE fails the sanity check in AbstractXAStatefulHolder.
// Even if the connection is shared (usageCount > 1), if the state was STATE_NOT_ACCESSIBLE
// we transition back to STATE_ACCESSIBLE.
if (usageCount == 1 || oldState == STATE_NOT_ACCESSIBLE) {
setState(STATE_ACCESSIBLE);
}
if (oldState == STATE_IN_POOL) {
if (log.isDebugEnabled()) log.debug("connection " + xaConnection + " was in state IN_POOL, testing it");
testConnection(connection);
applyIsolationLevel();
applyCursorHoldabilty();
if (TransactionContextHelper.currentTransaction() == null) {
// it is safe to set the auto-commit flag outside of a global transaction
applyLocalAutoCommit();
}
}
else {
if (log.isDebugEnabled()) log.debug("connection " + xaConnection + " was in state " + Decoder.decodeXAStatefulHolderState(oldState) + ", no need to test it");
}
if (log.isDebugEnabled()) log.debug("got connection handle from " + this);
return new JdbcConnectionHandle(this, connection);
}
public void stateChanged(XAStatefulHolder source, int oldState, int newState) {
if (newState == STATE_IN_POOL) {
if (log.isDebugEnabled()) log.debug("requeued JDBC connection of " + poolingDataSource);
lastReleaseDate = new Date();
}
if (oldState == STATE_IN_POOL && newState == STATE_ACCESSIBLE) {
acquisitionDate = new Date();
}
if (oldState == STATE_NOT_ACCESSIBLE && newState == STATE_ACCESSIBLE) {
TransactionContextHelper.recycle(this);
}
}
public void stateChanging(XAStatefulHolder source, int currentState, int futureState) {
if (futureState == STATE_IN_POOL) {
if (usageCount > 0) {
log.warn("usage count too high (" + usageCount + ") on connection returned to pool " + source);
}
}
if (futureState == STATE_IN_POOL || futureState == STATE_NOT_ACCESSIBLE) {
// close all uncached statements
if (log.isDebugEnabled()) log.debug("closing " + uncachedStatements.size() + " dangling uncached statement(s)");
for (int i = 0; i < uncachedStatements.size(); i++) {
Statement statement = (Statement) uncachedStatements.get(i);
try {
statement.close();
} catch (SQLException ex) {
if (log.isDebugEnabled()) log.debug("error trying to close uncached statement " + statement, ex);
}
}
uncachedStatements.clear();
// clear SQL warnings
try {
if (log.isDebugEnabled()) log.debug("clearing warnings of " + connection);
connection.clearWarnings();
} catch (SQLException ex) {
if (log.isDebugEnabled()) log.debug("error cleaning warnings of " + connection, ex);
}
}
}
/**
* Get a PreparedStatement from cache.
* @param stmt the key that has been used to cache the statement.
* @return the cached statement corresponding to the key or null if no statement is cached under that key.
*/
protected JdbcPreparedStatementHandle getCachedStatement(JdbcPreparedStatementHandle stmt) {
return statementsCache.get(stmt);
}
/**
* Put a PreparedStatement in the cache.
* @param stmt the statement to cache.
* @return the cached statement.
*/
protected JdbcPreparedStatementHandle putCachedStatement(JdbcPreparedStatementHandle stmt) {
return statementsCache.put(stmt);
}
/**
* Register uncached statement so that it can be closed when the connection is put back in the pool.
*
* @param stmt the statement to register.
* @return the registered statement.
*/
protected Statement registerUncachedStatement(Statement stmt) {
uncachedStatements.add(stmt);
return stmt;
}
protected void unregisterUncachedStatement(Statement stmt) {
uncachedStatements.remove(stmt);
}
public String toString() {
return "a JdbcPooledConnection from datasource " + poolingDataSource.getUniqueName() + " in state " + Decoder.decodeXAStatefulHolderState(getState()) + " with usage count " + usageCount + " wrapping " + xaConnection;
}
private void applyCursorHoldabilty() throws SQLException {
String cursorHoldability = getPoolingDataSource().getCursorHoldability();
if (cursorHoldability != null) {
int holdability = translateCursorHoldability(cursorHoldability);
if (holdability < 0) {
log.warn("invalid cursor holdability '" + cursorHoldability + "' configured, keeping the default cursor holdability.");
}
else {
if (log.isDebugEnabled()) log.debug("setting connection's cursor holdability to " + cursorHoldability);
connection.setHoldability(holdability);
}
}
}
private static int translateCursorHoldability(String cursorHoldability) {
if ("CLOSE_CURSORS_AT_COMMIT".equals(cursorHoldability)) return ResultSet.CLOSE_CURSORS_AT_COMMIT;
if ("HOLD_CURSORS_OVER_COMMIT".equals(cursorHoldability)) return ResultSet.HOLD_CURSORS_OVER_COMMIT;
return -1;
}
private void applyLocalAutoCommit() throws SQLException {
String localAutoCommit = getPoolingDataSource().getLocalAutoCommit();
if (localAutoCommit != null) {
if (localAutoCommit.equalsIgnoreCase("true")) {
if (log.isDebugEnabled()) log.debug("setting connection's auto commit to true");
connection.setAutoCommit(true);
}
else if (localAutoCommit.equalsIgnoreCase("false")) {
if (log.isDebugEnabled()) log.debug("setting connection's auto commit to false");
connection.setAutoCommit(false);
}
else {
log.warn("invalid auto commit '" + localAutoCommit + "' configured, keeping default auto commit");
}
}
}
/* management */
public String getStateDescription() {
return Decoder.decodeXAStatefulHolderState(getState());
}
public Date getAcquisitionDate() {
return acquisitionDate;
}
public Date getLastReleaseDate() {
return lastReleaseDate;
}
public Collection getTransactionGtridsCurrentlyHoldingThis() {
return getXAResourceHolderStateGtrids();
}
}