blob: 5a74747f526e7aa9634d4f5cd018c962ebb2aeda [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.dbcp.dbcp2;
import java.lang.management.ManagementFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import org.apache.tomcat.dbcp.pool2.ObjectPool;
/**
* A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
* when closed.
*
* @since 2.0
*/
public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
private static MBeanServer MBEAN_SERVER;
static {
try {
MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
} catch (NoClassDefFoundError | Exception ex) {
// ignore - JMX not available
}
}
/** The pool to which I should return. */
private final ObjectPool<PoolableConnection> pool;
private final ObjectNameWrapper jmxObjectName;
// Use a prepared statement for validation, retaining the last used SQL to
// check if the validation query has changed.
private PreparedStatement validationPreparedStatement;
private String lastValidationSql;
/**
* Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
* considered broken and not pass validation in the future.
*/
private boolean fatalSqlExceptionThrown = false;
/**
* SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
* {@link Utils#DISCONNECTION_SQL_CODES} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
*/
private final Collection<String> disconnectionSqlCodes;
/** Whether or not to fast fail validation after fatal connection errors */
private final boolean fastFailValidation;
/**
*
* @param conn
* my underlying connection
* @param pool
* the pool to which I should return when closed
* @param jmxObjectName
* JMX name
* @param disconnectSqlCodes
* SQL_STATE codes considered fatal disconnection errors
* @param fastFailValidation
* true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
* run query or isValid)
*/
public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
final boolean fastFailValidation) {
super(conn);
this.pool = pool;
this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
this.disconnectionSqlCodes = disconnectSqlCodes;
this.fastFailValidation = fastFailValidation;
if (jmxObjectName != null) {
try {
MBEAN_SERVER.registerMBean(this, jmxObjectName);
} catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
// For now, simply skip registration
}
}
}
/**
*
* @param conn
* my underlying connection
* @param pool
* the pool to which I should return when closed
* @param jmxName
* JMX name
*/
public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
final ObjectName jmxName) {
this(conn, pool, jmxName, null, false);
}
@Override
protected void passivate() throws SQLException {
super.passivate();
setClosedInternal(true);
}
/**
* {@inheritDoc}
* <p>
* This method should not be used by a client to determine whether or not a connection should be return to the
* connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool
* once it is no longer required.
*/
@Override
public boolean isClosed() throws SQLException {
if (isClosedInternal()) {
return true;
}
if (getDelegateInternal().isClosed()) {
// Something has gone wrong. The underlying connection has been
// closed without the connection being returned to the pool. Return
// it now.
close();
return true;
}
return false;
}
/**
* Returns me to my pool.
*/
@Override
public synchronized void close() throws SQLException {
if (isClosedInternal()) {
// already closed
return;
}
boolean isUnderlyingConnectionClosed;
try {
isUnderlyingConnectionClosed = getDelegateInternal().isClosed();
} catch (final SQLException e) {
try {
pool.invalidateObject(this);
} catch (final IllegalStateException ise) {
// pool is closed, so close the connection
passivate();
getInnermostDelegate().close();
} catch (final Exception ie) {
// DO NOTHING the original exception will be rethrown
}
throw new SQLException("Cannot close connection (isClosed check failed)", e);
}
/*
* Can't set close before this code block since the connection needs to be open when validation runs. Can't set
* close after this code block since by then the connection will have been returned to the pool and may have
* been borrowed by another thread. Therefore, the close flag is set in passivate().
*/
if (isUnderlyingConnectionClosed) {
// Abnormal close: underlying connection closed unexpectedly, so we
// must destroy this proxy
try {
pool.invalidateObject(this);
} catch (final IllegalStateException e) {
// pool is closed, so close the connection
passivate();
getInnermostDelegate().close();
} catch (final Exception e) {
throw new SQLException("Cannot close connection (invalidating pooled object failed)", e);
}
} else {
// Normal close: underlying connection is still open, so we
// simply need to return this proxy to the pool
try {
pool.returnObject(this);
} catch (final IllegalStateException e) {
// pool is closed, so close the connection
passivate();
getInnermostDelegate().close();
} catch (final SQLException e) {
throw e;
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new SQLException("Cannot close connection (return to pool failed)", e);
}
}
}
/**
* Actually close my underlying {@link Connection}.
*/
@Override
public void reallyClose() throws SQLException {
if (jmxObjectName != null) {
jmxObjectName.unregisterMBean();
}
if (validationPreparedStatement != null) {
try {
validationPreparedStatement.close();
} catch (final SQLException sqle) {
// Ignore
}
}
super.closeInternal();
}
/**
* Expose the {@link #toString()} method via a bean getter so it can be read as a property via JMX.
*/
@Override
public String getToString() {
return toString();
}
/**
* Validates the connection, using the following algorithm:
* <ol>
* <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
* thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
* <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
* returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
* <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
* least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
* </ol>
*
* @param sql
* The validation SQL query.
* @param timeoutSeconds
* The validation timeout in seconds.
* @throws SQLException
* Thrown when validation fails or an SQLException occurs during validation
*/
public void validate(final String sql, int timeoutSeconds) throws SQLException {
if (fastFailValidation && fatalSqlExceptionThrown) {
throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail"));
}
if (sql == null || sql.length() == 0) {
if (timeoutSeconds < 0) {
timeoutSeconds = 0;
}
if (!isValid(timeoutSeconds)) {
throw new SQLException("isValid() returned false");
}
return;
}
if (!sql.equals(lastValidationSql)) {
lastValidationSql = sql;
// Has to be the innermost delegate else the prepared statement will
// be closed when the pooled connection is passivated.
validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql);
}
if (timeoutSeconds > 0) {
validationPreparedStatement.setQueryTimeout(timeoutSeconds);
}
try (ResultSet rs = validationPreparedStatement.executeQuery()) {
if (!rs.next()) {
throw new SQLException("validationQuery didn't return a row");
}
} catch (final SQLException sqle) {
throw sqle;
}
}
/**
* Checks the SQLState of the input exception and any nested SQLExceptions it wraps.
* <p>
* If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the
* configured list of fatal exception codes. If this property is not set, codes are compared against the default
* codes in {@link Utils#DISCONNECTION_SQL_CODES} and in this case anything starting with #{link
* Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
* </p>
*
* @param e
* SQLException to be examined
* @return true if the exception signals a disconnection
*/
private boolean isDisconnectionSqlException(final SQLException e) {
boolean fatalException = false;
final String sqlState = e.getSQLState();
if (sqlState != null) {
fatalException = disconnectionSqlCodes == null
? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX)
|| Utils.DISCONNECTION_SQL_CODES.contains(sqlState)
: disconnectionSqlCodes.contains(sqlState);
if (!fatalException) {
final SQLException nextException = e.getNextException();
if (nextException != null && nextException != e) {
fatalException = isDisconnectionSqlException(e.getNextException());
}
}
}
return fatalException;
}
@Override
protected void handleException(final SQLException e) throws SQLException {
fatalSqlExceptionThrown |= isDisconnectionSqlException(e);
super.handleException(e);
}
}