blob: 42f38c77323d171107087b197ab539a297c7f79c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.tomcat.jdbc.pool;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;
/**
* Represents a pooled connection
* and holds a reference to the {@link java.sql.Connection} object
* @version 1.0
*/
public class PooledConnection {
/**
* Logger
*/
private static final Log log = LogFactory.getLog(PooledConnection.class);
public static final String PROP_USER = PoolUtilities.PROP_USER;
public static final String PROP_PASSWORD = PoolUtilities.PROP_PASSWORD;
/**
* Validate when connection is borrowed flag
*/
public static final int VALIDATE_BORROW = 1;
/**
* Validate when connection is returned flag
*/
public static final int VALIDATE_RETURN = 2;
/**
* Validate when connection is idle flag
*/
public static final int VALIDATE_IDLE = 3;
/**
* Validate when connection is initialized flag
*/
public static final int VALIDATE_INIT = 4;
/**
* The properties for the connection pool
*/
protected PoolConfiguration poolProperties;
/**
* The underlying database connection
*/
private volatile java.sql.Connection connection;
/**
* If using a XAConnection underneath.
*/
protected volatile javax.sql.XAConnection xaConnection;
/**
* When we track abandon traces, this string holds the thread dump
*/
private String abandonTrace = null;
/**
* Timestamp the connection was last 'touched' by the pool
*/
private volatile long timestamp;
/**
* Lock for this connection only
*/
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
/**
* Set to true if this connection has been discarded by the pool
*/
private volatile boolean discarded = false;
/**
* The Timestamp when the last time the connect() method was called successfully
*/
private volatile long lastConnected = -1;
/**
* timestamp to keep track of validation intervals
*/
private volatile long lastValidated = System.currentTimeMillis();
/**
* The parent
*/
protected ConnectionPool parent;
private HashMap<Object, Object> attributes = new HashMap<>();
private volatile long connectionVersion=0;
/**
* Weak reference to cache the list of interceptors for this connection
* so that we don't create a new list of interceptors each time we borrow
* the connection
*/
private volatile JdbcInterceptor handler = null;
private AtomicBoolean released = new AtomicBoolean(false);
private volatile boolean suspect = false;
private java.sql.Driver driver = null;
/**
* Constructor
* @param prop - pool properties
* @param parent - the parent connection pool
*/
public PooledConnection(PoolConfiguration prop, ConnectionPool parent) {
poolProperties = prop;
this.parent = parent;
connectionVersion = parent.getPoolVersion();
}
public long getConnectionVersion() {
return connectionVersion;
}
/**
* @deprecated use {@link #shouldForceReconnect(String, String)}
* method kept since it was public, to avoid changing interface. name was pooo
*/
@Deprecated
public boolean checkUser(String username, String password) {
return !shouldForceReconnect(username, password);
}
/**
* Returns true if we must force reconnect based on credentials passed in.
* Returns false if {@link PoolConfiguration#isAlternateUsernameAllowed()} method returns false.
* Returns false if the username/password has not changed since this connection was connected
* @param username the username you wish to connect with, pass in null to accept the default username from {@link PoolConfiguration#getUsername()}
* @param password the password you wish to connect with, pass in null to accept the default username from {@link org.apache.tomcat.jdbc.pool.PoolConfiguration#getPassword()}
* @return true is the pool must reconnect
*/
public boolean shouldForceReconnect(String username, String password) {
if (!getPoolProperties().isAlternateUsernameAllowed()) return false;
if (username==null) username = poolProperties.getUsername();
if (password==null) password = poolProperties.getPassword();
String storedUsr = (String)getAttributes().get(PROP_USER);
String storedPwd = (String)getAttributes().get(PROP_PASSWORD);
boolean noChangeInCredentials = (username==null && storedUsr==null);
noChangeInCredentials = (noChangeInCredentials || (username!=null && username.equals(storedUsr)));
noChangeInCredentials = noChangeInCredentials && ((password==null && storedPwd==null) || (password!=null && password.equals(storedPwd)));
if (username==null) getAttributes().remove(PROP_USER); else getAttributes().put(PROP_USER, username);
if (password==null) getAttributes().remove(PROP_PASSWORD); else getAttributes().put(PROP_PASSWORD, password);
return !noChangeInCredentials;
}
/**
* Connects the underlying connection to the database.
* @throws SQLException if the method {@link #release()} has been called.
* @throws SQLException if driver instantiation fails
* @throws SQLException if a call to {@link java.sql.Driver#connect(String, java.util.Properties)} fails.
* @throws SQLException if default properties are configured and a call to
* {@link java.sql.Connection#setAutoCommit(boolean)}, {@link java.sql.Connection#setCatalog(String)},
* {@link java.sql.Connection#setTransactionIsolation(int)} or {@link java.sql.Connection#setReadOnly(boolean)} fails.
*/
public void connect() throws SQLException {
if (released.get()) throw new SQLException("A connection once released, can't be reestablished.");
if (connection != null) {
try {
this.disconnect(false);
} catch (Exception x) {
log.debug("Unable to disconnect previous connection.", x);
} //catch
} //end if
if (poolProperties.getDataSource()==null && poolProperties.getDataSourceJNDI()!=null) {
//TODO lookup JNDI name
}
if (poolProperties.getDataSource()!=null) {
connectUsingDataSource();
} else {
connectUsingDriver();
}
//set up the default state, unless we expect the interceptor to do it
if (poolProperties.getJdbcInterceptors()==null || poolProperties.getJdbcInterceptors().indexOf(ConnectionState.class.getName())<0 ||
poolProperties.getJdbcInterceptors().indexOf(ConnectionState.class.getSimpleName())<0) {
if (poolProperties.getDefaultTransactionIsolation()!=DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION) connection.setTransactionIsolation(poolProperties.getDefaultTransactionIsolation());
if (poolProperties.getDefaultReadOnly()!=null) connection.setReadOnly(poolProperties.getDefaultReadOnly().booleanValue());
if (poolProperties.getDefaultAutoCommit()!=null) connection.setAutoCommit(poolProperties.getDefaultAutoCommit().booleanValue());
if (poolProperties.getDefaultCatalog()!=null) connection.setCatalog(poolProperties.getDefaultCatalog());
}
this.discarded = false;
this.lastConnected = System.currentTimeMillis();
}
protected void connectUsingDataSource() throws SQLException {
String usr = null;
String pwd = null;
if (getAttributes().containsKey(PROP_USER)) {
usr = (String) getAttributes().get(PROP_USER);
} else {
usr = poolProperties.getUsername();
getAttributes().put(PROP_USER, usr);
}
if (getAttributes().containsKey(PROP_PASSWORD)) {
pwd = (String) getAttributes().get(PROP_PASSWORD);
} else {
pwd = poolProperties.getPassword();
getAttributes().put(PROP_PASSWORD, pwd);
}
if (poolProperties.getDataSource() instanceof javax.sql.XADataSource) {
javax.sql.XADataSource xds = (javax.sql.XADataSource)poolProperties.getDataSource();
if (usr!=null && pwd!=null) {
xaConnection = xds.getXAConnection(usr, pwd);
connection = xaConnection.getConnection();
} else {
xaConnection = xds.getXAConnection();
connection = xaConnection.getConnection();
}
} else if (poolProperties.getDataSource() instanceof javax.sql.DataSource){
javax.sql.DataSource ds = (javax.sql.DataSource)poolProperties.getDataSource();
if (usr!=null && pwd!=null) {
connection = ds.getConnection(usr, pwd);
} else {
connection = ds.getConnection();
}
} else if (poolProperties.getDataSource() instanceof javax.sql.ConnectionPoolDataSource){
javax.sql.ConnectionPoolDataSource ds = (javax.sql.ConnectionPoolDataSource)poolProperties.getDataSource();
if (usr!=null && pwd!=null) {
connection = ds.getPooledConnection(usr, pwd).getConnection();
} else {
connection = ds.getPooledConnection().getConnection();
}
} else {
throw new SQLException("DataSource is of unknown class:"+(poolProperties.getDataSource()!=null?poolProperties.getDataSource().getClass():"null"));
}
}
protected void connectUsingDriver() throws SQLException {
try {
if (driver==null) {
if (log.isDebugEnabled()) {
log.debug("Instantiating driver using class: "+poolProperties.getDriverClassName()+" [url="+poolProperties.getUrl()+"]");
}
if (poolProperties.getDriverClassName()==null) {
//rely on DriverManager
log.warn("Not loading a JDBC driver as driverClassName property is null.");
} else {
driver = (java.sql.Driver)
ClassLoaderUtil.loadClass(
poolProperties.getDriverClassName(),
PooledConnection.class.getClassLoader(),
Thread.currentThread().getContextClassLoader()
).getConstructor().newInstance();
}
}
} catch (java.lang.Exception cn) {
if (log.isDebugEnabled()) {
log.debug("Unable to instantiate JDBC driver.", cn);
}
SQLException ex = new SQLException(cn.getMessage());
ex.initCause(cn);
throw ex;
}
String driverURL = poolProperties.getUrl();
String usr = null;
String pwd = null;
if (getAttributes().containsKey(PROP_USER)) {
usr = (String) getAttributes().get(PROP_USER);
} else {
usr = poolProperties.getUsername();
getAttributes().put(PROP_USER, usr);
}
if (getAttributes().containsKey(PROP_PASSWORD)) {
pwd = (String) getAttributes().get(PROP_PASSWORD);
} else {
pwd = poolProperties.getPassword();
getAttributes().put(PROP_PASSWORD, pwd);
}
Properties properties = PoolUtilities.clone(poolProperties.getDbProperties());
if (usr != null) properties.setProperty(PROP_USER, usr);
if (pwd != null) properties.setProperty(PROP_PASSWORD, pwd);
try {
if (driver==null) {
connection = DriverManager.getConnection(driverURL, properties);
} else {
connection = driver.connect(driverURL, properties);
}
} catch (Exception x) {
if (log.isDebugEnabled()) {
log.debug("Unable to connect to database.", x);
}
if (parent.jmxPool!=null) {
parent.jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_CONNECT,
ConnectionPool.getStackTrace(x));
}
if (x instanceof SQLException) {
throw (SQLException)x;
} else {
SQLException ex = new SQLException(x.getMessage());
ex.initCause(x);
throw ex;
}
}
if (connection==null) {
throw new SQLException("Driver:"+driver+" returned null for URL:"+driverURL);
}
}
/**
*
* @return true if connect() was called successfully and disconnect has not yet been called
*/
public boolean isInitialized() {
return connection!=null;
}
/**
* Returns true if the connection has been connected more than
* {@link PoolConfiguration#getMaxAge()} milliseconds. false otherwise.
* @return Returns true if the connection has been connected more than
* {@link PoolConfiguration#getMaxAge()} milliseconds. false otherwise.
*/
public boolean isMaxAgeExpired() {
if (getPoolProperties().getMaxAge()>0 ) {
return (System.currentTimeMillis() - getLastConnected()) > getPoolProperties().getMaxAge();
} else {
return false;
}
}
/**
* Issues a call to {@link #disconnect(boolean)} with the argument false followed by a call to
* {@link #connect()}
* @throws SQLException if the call to {@link #connect()} fails.
*/
public void reconnect() throws SQLException {
this.disconnect(false);
this.connect();
} //reconnect
/**
* Disconnects the connection. All exceptions are logged using debug level.
* @param finalize if set to true, a call to {@link ConnectionPool#finalize(PooledConnection)} is called.
*/
private void disconnect(boolean finalize) {
if (isDiscarded() && connection == null) {
return;
}
setDiscarded(true);
if (connection != null) {
try {
parent.disconnectEvent(this, finalize);
if (xaConnection == null) {
connection.close();
} else {
xaConnection.close();
}
}catch (Exception ignore) {
if (log.isDebugEnabled()) {
log.debug("Unable to close underlying SQL connection",ignore);
}
}
}
connection = null;
xaConnection = null;
lastConnected = -1;
if (finalize) parent.finalize(this);
}
//============================================================================
//
//============================================================================
/**
* Returns abandon timeout in milliseconds
* @return abandon timeout in milliseconds
*/
public long getAbandonTimeout() {
if (poolProperties.getRemoveAbandonedTimeout() <= 0) {
return Long.MAX_VALUE;
} else {
return poolProperties.getRemoveAbandonedTimeout() * 1000L;
} //end if
}
/**
* Returns true if the connection pool is configured
* to do validation for a certain action.
* @param action
*/
private boolean doValidate(int action) {
if (action == PooledConnection.VALIDATE_BORROW &&
poolProperties.isTestOnBorrow())
return true;
else if (action == PooledConnection.VALIDATE_RETURN &&
poolProperties.isTestOnReturn())
return true;
else if (action == PooledConnection.VALIDATE_IDLE &&
poolProperties.isTestWhileIdle())
return true;
else if (action == PooledConnection.VALIDATE_INIT &&
poolProperties.isTestOnConnect())
return true;
else if (action == PooledConnection.VALIDATE_INIT &&
poolProperties.getInitSQL()!=null)
return true;
else
return false;
}
/**Returns true if the object is still valid. if not
* the pool will call the getExpiredAction() and follow up with one
* of the four expired methods
*/
public boolean validate(int validateAction) {
return validate(validateAction,null);
}
/**
* Validates a connection.
* @param validateAction the action used. One of {@link #VALIDATE_BORROW}, {@link #VALIDATE_IDLE},
* {@link #VALIDATE_INIT} or {@link #VALIDATE_RETURN}
* @param sql the SQL to be used during validation. If the {@link PoolConfiguration#setInitSQL(String)} has been called with a non null
* value and the action is {@link #VALIDATE_INIT} the init SQL will be used for validation.
*
* @return true if the connection was validated successfully. It returns true even if validation was not performed, such as when
* {@link PoolConfiguration#setValidationInterval(long)} has been called with a positive value.
* <p>
* false if the validation failed. The caller should close the connection if false is returned since a session could have been left in
* an unknown state during initialization.
*/
public boolean validate(int validateAction,String sql) {
if (this.isDiscarded()) {
return false;
}
if (!doValidate(validateAction)) {
//no validation required, no init sql and props not set
return true;
}
//Don't bother validating if already have recently enough
long now = System.currentTimeMillis();
if (validateAction!=VALIDATE_INIT &&
poolProperties.getValidationInterval() > 0 &&
(now - this.lastValidated) <
poolProperties.getValidationInterval()) {
return true;
}
if (poolProperties.getValidator() != null) {
if (poolProperties.getValidator().validate(connection, validateAction)) {
this.lastValidated = now;
return true;
} else {
if (getPoolProperties().getLogValidationErrors()) {
log.error("Custom validation through "+poolProperties.getValidator()+" failed.");
}
return false;
}
}
String query = sql;
if (validateAction == VALIDATE_INIT && poolProperties.getInitSQL() != null) {
query = poolProperties.getInitSQL();
}
if (query == null) {
query = poolProperties.getValidationQuery();
}
if (query == null) {
int validationQueryTimeout = poolProperties.getValidationQueryTimeout();
if (validationQueryTimeout < 0) validationQueryTimeout = 0;
try {
if (connection.isValid(validationQueryTimeout)) {
this.lastValidated = now;
return true;
} else {
if (getPoolProperties().getLogValidationErrors()) {
log.error("isValid() returned false.");
}
return false;
}
} catch (SQLException e) {
if (getPoolProperties().getLogValidationErrors()) {
log.error("isValid() failed.", e);
} else if (log.isDebugEnabled()) {
log.debug("isValid() failed.", e);
}
return false;
}
}
Statement stmt = null;
try {
stmt = connection.createStatement();
int validationQueryTimeout = poolProperties.getValidationQueryTimeout();
if (validationQueryTimeout > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
stmt.execute(query);
stmt.close();
this.lastValidated = now;
return true;
} catch (Exception ex) {
if (getPoolProperties().getLogValidationErrors()) {
log.error("SQL Validation error", ex);
} else if (log.isDebugEnabled()) {
log.debug("Unable to validate object:",ex);
}
if (stmt!=null)
try { stmt.close();} catch (Exception ignore2){/*NOOP*/}
try {
if(!connection.getAutoCommit()) {
connection.rollback();
}
} catch (SQLException e) {
// do nothing
}
} finally {
try {
if(!connection.getAutoCommit()) {
connection.commit();
}
} catch (SQLException e) {
// do nothing
}
}
return false;
} //validate
/**
* The time limit for how long the object
* can remain unused before it is released
* @return {@link PoolConfiguration#getMinEvictableIdleTimeMillis()}
*/
public long getReleaseTime() {
return this.poolProperties.getMinEvictableIdleTimeMillis();
}
/**
* This method is called if (Now - timeCheckedIn &gt; getReleaseTime())
* This method disconnects the connection, logs an error in debug mode if it happens
* then sets the {@link #released} flag to false. Any attempts to connect this cached object again
* will fail per {@link #connect()}
* The connection pool uses the atomic return value to decrement the pool size counter.
* @return true if this is the first time this method has been called. false if this method has been called before.
*/
public boolean release() {
try {
disconnect(true);
} catch (Exception x) {
if (log.isDebugEnabled()) {
log.debug("Unable to close SQL connection",x);
}
}
return released.compareAndSet(false, true);
}
/**
* The pool will set the stack trace when it is check out and
* checked in
* @param trace the stack trace for this connection
*/
public void setStackTrace(String trace) {
abandonTrace = trace;
}
/**
* Returns the stack trace from when this connection was borrowed. Can return null if no stack trace was set.
* @return the stack trace or null of no trace was set
*/
public String getStackTrace() {
return abandonTrace;
}
/**
* Sets a timestamp on this connection. A timestamp usually means that some operation
* performed successfully.
* @param timestamp the timestamp as defined by {@link System#currentTimeMillis()}
*/
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
setSuspect(false);
}
public boolean isSuspect() {
return suspect;
}
public void setSuspect(boolean suspect) {
this.suspect = suspect;
}
/**
* An interceptor can call this method with the value true, and the connection will be closed when it is returned to the pool.
* @param discarded - only valid value is true
* @throws IllegalStateException if this method is called with the value false and the value true has already been set.
*/
public void setDiscarded(boolean discarded) {
if (this.discarded && !discarded) throw new IllegalStateException("Unable to change the state once the connection has been discarded");
this.discarded = discarded;
}
/**
* Set the timestamp the connection was last validated.
* This flag is used to keep track when we are using a {@link PoolConfiguration#setValidationInterval(long) validation-interval}.
* @param lastValidated a timestamp as defined by {@link System#currentTimeMillis()}
*/
public void setLastValidated(long lastValidated) {
this.lastValidated = lastValidated;
}
/**
* Sets the pool configuration for this connection and connection pool.
* Object is shared with the {@link ConnectionPool}
* @param poolProperties
*/
public void setPoolProperties(PoolConfiguration poolProperties) {
this.poolProperties = poolProperties;
}
/**
* Return the timestamps of last pool action. Timestamps are typically set when connections
* are borrowed from the pool. It is used to keep track of {@link PoolConfiguration#setRemoveAbandonedTimeout(int) abandon-timeouts}.
* This timestamp can also be reset by the {@link org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer#invoke(Object, java.lang.reflect.Method, Object[])}
* @return the timestamp of the last pool action as defined by {@link System#currentTimeMillis()}
*/
public long getTimestamp() {
return timestamp;
}
/**
* Returns the discarded flag.
* @return the discarded flag. If the value is true,
* either {@link #disconnect(boolean)} has been called or it will be called when the connection is returned to the pool.
*/
public boolean isDiscarded() {
return discarded;
}
/**
* Returns the timestamp of the last successful validation query execution.
* @return the timestamp of the last successful validation query execution as defined by {@link System#currentTimeMillis()}
*/
public long getLastValidated() {
return lastValidated;
}
/**
* Returns the configuration for this connection and pool
* @return the configuration for this connection and pool
*/
public PoolConfiguration getPoolProperties() {
return poolProperties;
}
/**
* Locks the connection only if either {@link PoolConfiguration#isPoolSweeperEnabled()} or
* {@link PoolConfiguration#getUseLock()} return true. The per connection lock ensures thread safety is
* multiple threads are performing operations on the connection.
* Otherwise this is a noop for performance
*/
public void lock() {
if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) {
//optimized, only use a lock when there is concurrency
lock.writeLock().lock();
}
}
/**
* Unlocks the connection only if the sweeper is enabled
* Otherwise this is a noop for performance
*/
public void unlock() {
if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) {
//optimized, only use a lock when there is concurrency
lock.writeLock().unlock();
}
}
/**
* Returns the underlying connection
* @return the underlying JDBC connection as it was returned from the JDBC driver
* @see javax.sql.PooledConnection#getConnection()
*/
public java.sql.Connection getConnection() {
return this.connection;
}
/**
* Returns the underlying XA connection
* @return the underlying XA connection as it was returned from the Datasource
*/
public javax.sql.XAConnection getXAConnection() {
return this.xaConnection;
}
/**
* Returns the timestamp of when the connection was last connected to the database.
* ie, a successful call to {@link java.sql.Driver#connect(String, java.util.Properties)}.
* @return the timestamp when this connection was created as defined by {@link System#currentTimeMillis()}
*/
public long getLastConnected() {
return lastConnected;
}
/**
* Returns the first handler in the interceptor chain
* @return the first interceptor for this connection
*/
public JdbcInterceptor getHandler() {
return handler;
}
public void setHandler(JdbcInterceptor handler) {
if (this.handler!=null && this.handler!=handler) {
JdbcInterceptor interceptor = this.handler;
while (interceptor!=null) {
interceptor.reset(null, null);
interceptor = interceptor.getNext();
}//while
}//end if
this.handler = handler;
}
@Override
public String toString() {
return "PooledConnection["+(connection!=null?connection.toString():"null")+"]";
}
/**
* Returns true if this connection has been released and wont be reused.
* @return true if the method {@link #release()} has been called
*/
public boolean isReleased() {
return released.get();
}
public HashMap<Object,Object> getAttributes() {
return attributes;
}
public void clearWarnings() {
try {
connection.clearWarnings();
} catch (SQLException e) {
log.warn("Unable to clear Warnings, connection will be closed.", e);
}
}
}