    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.dbcp2;
    018
    019import java.lang.management.ManagementFactory;
    020import java.sql.Connection;
    021import java.sql.PreparedStatement;
    022import java.sql.ResultSet;
    023import java.sql.SQLException;
    024import java.util.Collection;
    025
    026import javax.management.InstanceAlreadyExistsException;
    027import javax.management.MBeanRegistrationException;
    028import javax.management.MBeanServer;
    029import javax.management.NotCompliantMBeanException;
    030import javax.management.ObjectName;
    031
    032import org.apache.commons.pool2.ObjectPool;
    033
    034/**
    035 * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
    036 * when closed.
    037 *
    038 * @since 2.0
    039 */
    040public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
    041
    042    private static MBeanServer MBEAN_SERVER;
    043
    044    static {
    045        try {
    046            MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
    047        } catch (NoClassDefFoundError | Exception ex) {
    048            // ignore - JMX not available
    049        }
    050    }
    051
    052    /** The pool to which I should return. */
    053    private final ObjectPool<PoolableConnection> pool;
    054
    055    private final ObjectNameWrapper jmxObjectName;
    056
    057    // Use a prepared statement for validation, retaining the last used SQL to
    058    // check if the validation query has changed.
    059    private PreparedStatement validationPreparedStatement;
    060    private String lastValidationSql;
    061
    062    /**
    063     * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
    064     * considered broken and not pass validation in the future.
    065     */
    066    private boolean fatalSqlExceptionThrown = false;
    067
    068    /**
    069     * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
    070     * {@link Utils#DISCONNECTION_SQL_CODES} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
    071     */
    072    private final Collection<String> disconnectionSqlCodes;
    073
    074    /** Whether or not to fast fail validation after fatal connection errors */
    075    private final boolean fastFailValidation;
    076
    077    /**
    078     *
    079     * @param conn
    080     *            my underlying connection
    081     * @param pool
    082     *            the pool to which I should return when closed
    083     * @param jmxObjectName
    084     *            JMX name
    085     * @param disconnectSqlCodes
    086     *            SQL_STATE codes considered fatal disconnection errors
    087     * @param fastFailValidation
    088     *            true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
    089     *            run query or isValid)
    090     */
    091    public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
    092            final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
    093            final boolean fastFailValidation) {
    094        super(conn);
    095        this.pool = pool;
    096        this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
    097        this.disconnectionSqlCodes = disconnectSqlCodes;
    098        this.fastFailValidation = fastFailValidation;
    099
    100        if (jmxObjectName != null) {
    101            try {
    102                MBEAN_SERVER.registerMBean(this, jmxObjectName);
    103            } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
    104                // For now, simply skip registration
    105            }
    106        }
    107    }
    108
    109    /**
    110     *
    111     * @param conn
    112     *            my underlying connection
    113     * @param pool
    114     *            the pool to which I should return when closed
    115     * @param jmxName
    116     *            JMX name
    117     */
    118    public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
    119            final ObjectName jmxName) {
    120        this(conn, pool, jmxName, null, false);
    121    }
    122
    123    @Override
    124    protected void passivate() throws SQLException {
    125        super.passivate();
    126        setClosedInternal(true);
    127    }
    128
    129    /**
    130     * {@inheritDoc}
    131     * <p>
    132     * This method should not be used by a client to determine whether or not a connection should be return to the
    133     * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool
    134     * once it is no longer required.
    135     */
    136    @Override
    137    public boolean isClosed() throws SQLException {
    138        if (isClosedInternal()) {
    139            return true;
    140        }
    141
    142        if (getDelegateInternal().isClosed()) {
    143            // Something has gone wrong. The underlying connection has been
    144            // closed without the connection being returned to the pool. Return
    145            // it now.
    146            close();
    147            return true;
    148        }
    149
    150        return false;
    151    }
    152
    153    /**
    154     * Returns me to my pool.
    155     */
    156    @Override
    157    public synchronized void close() throws SQLException {
    158        if (isClosedInternal()) {
    159            // already closed
    160            return;
    161        }
    162
    163        boolean isUnderlyingConnectionClosed;
    164        try {
    165            isUnderlyingConnectionClosed = getDelegateInternal().isClosed();
    166        } catch (final SQLException e) {
    167            try {
    168                pool.invalidateObject(this);
    169            } catch (final IllegalStateException ise) {
    170                // pool is closed, so close the connection
    171                passivate();
    172                getInnermostDelegate().close();
    173            } catch (final Exception ie) {
    174                // DO NOTHING the original exception will be rethrown
    175            }
    176            throw new SQLException("Cannot close connection (isClosed check failed)", e);
    177        }
    178
    179        /*
    180         * Can't set close before this code block since the connection needs to be open when validation runs. Can't set
    181         * close after this code block since by then the connection will have been returned to the pool and may have
    182         * been borrowed by another thread. Therefore, the close flag is set in passivate().
    183         */
    184        if (isUnderlyingConnectionClosed) {
    185            // Abnormal close: underlying connection closed unexpectedly, so we
    186            // must destroy this proxy
    187            try {
    188                pool.invalidateObject(this);
    189            } catch (final IllegalStateException e) {
    190                // pool is closed, so close the connection
    191                passivate();
    192                getInnermostDelegate().close();
    193            } catch (final Exception e) {
    194                throw new SQLException("Cannot close connection (invalidating pooled object failed)", e);
    195            }
    196        } else {
    197            // Normal close: underlying connection is still open, so we
    198            // simply need to return this proxy to the pool
    199            try {
    200                pool.returnObject(this);
    201            } catch (final IllegalStateException e) {
    202                // pool is closed, so close the connection
    203                passivate();
    204                getInnermostDelegate().close();
    205            } catch (final SQLException e) {
    206                throw e;
    207            } catch (final RuntimeException e) {
    208                throw e;
    209            } catch (final Exception e) {
    210                throw new SQLException("Cannot close connection (return to pool failed)", e);
    211            }
    212        }
    213    }
    214
    215    /**
    216     * Actually close my underlying {@link Connection}.
    217     */
    218    @Override
    219    public void reallyClose() throws SQLException {
    220        if (jmxObjectName != null) {
    221            jmxObjectName.unregisterMBean();
    222        }
    223
    224        if (validationPreparedStatement != null) {
    225            try {
    226                validationPreparedStatement.close();
    227            } catch (final SQLException sqle) {
    228                // Ignore
    229            }
    230        }
    231
    232        super.closeInternal();
    233    }
    234
    235    /**
    236     * Expose the {@link #toString()} method via a bean getter so it can be read as a property via JMX.
    237     */
    238    @Override
    239    public String getToString() {
    240        return toString();
    241    }
    242
    243    /**
    244     * Validates the connection, using the following algorithm:
    245     * <ol>
    246     * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
    247     * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
    248     * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
    249     * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
    250     * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
    251     * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
    252     * </ol>
    253     *
    254     * @param sql
    255     *            The validation SQL query.
    256     * @param timeoutSeconds
    257     *            The validation timeout in seconds.
    258     * @throws SQLException
    259     *             Thrown when validation fails or an SQLException occurs during validation
    260     */
    261    public void validate(final String sql, int timeoutSeconds) throws SQLException {
    262        if (fastFailValidation && fatalSqlExceptionThrown) {
    263            throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail"));
    264        }
    265
    266        if (sql == null || sql.length() == 0) {
    267            if (timeoutSeconds < 0) {
    268                timeoutSeconds = 0;
    269            }
    270            if (!isValid(timeoutSeconds)) {
    271                throw new SQLException("isValid() returned false");
    272            }
    273            return;
    274        }
    275
    276        if (!sql.equals(lastValidationSql)) {
    277            lastValidationSql = sql;
    278            // Has to be the innermost delegate else the prepared statement will
    279            // be closed when the pooled connection is passivated.
    280            validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql);
    281        }
    282
    283        if (timeoutSeconds > 0) {
    284            validationPreparedStatement.setQueryTimeout(timeoutSeconds);
    285        }
    286
    287        try (ResultSet rs = validationPreparedStatement.executeQuery()) {
    288            if (!rs.next()) {
    289                throw new SQLException("validationQuery didn't return a row");
    290            }
    291        } catch (final SQLException sqle) {
    292            throw sqle;
    293        }
    294    }
    295
    296    /**
    297     * Checks the SQLState of the input exception and any nested SQLExceptions it wraps.
    298     * <p>
    299     * If {@link #getDisconnectSqlCodes() disconnectSQLCodes} has been set, sql states are compared to those in the
    300     * configured list of fatal exception codes. If this property is not set, codes are compared against the default
    301     * codes in #{@link Utils.DISCONNECTION_SQL_CODES} and in this case anything starting with #{link
    302     * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
    303     * </p>
    304     *
    305     * @param e
    306     *            SQLException to be examined
    307     * @return true if the exception signals a disconnection
    308     */
    309    private boolean isDisconnectionSqlException(final SQLException e) {
    310        boolean fatalException = false;
    311        final String sqlState = e.getSQLState();
    312        if (sqlState != null) {
    313            fatalException = disconnectionSqlCodes == null
    314                    ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX)
    315                            || Utils.DISCONNECTION_SQL_CODES.contains(sqlState)
    316                    : disconnectionSqlCodes.contains(sqlState);
    317            if (!fatalException) {
    318                final SQLException nextException = e.getNextException();
    319                if (nextException != null && nextException != e) {
    320                    fatalException = isDisconnectionSqlException(e.getNextException());
    321                }
    322            }
    323        }
    324        return fatalException;
    325    }
    326
    327    @Override
    328    protected void handleException(final SQLException e) throws SQLException {
    329        fatalSqlExceptionThrown |= isDisconnectionSqlException(e);
    330        super.handleException(e);
    331    }
    332}