    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.time.Duration;
    025import java.util.Collection;
    026import java.util.concurrent.Executor;
    027
    028import javax.management.InstanceAlreadyExistsException;
    029import javax.management.MBeanRegistrationException;
    030import javax.management.MBeanServer;
    031import javax.management.NotCompliantMBeanException;
    032import javax.management.ObjectName;
    033
    034import org.apache.commons.pool2.ObjectPool;
    035
    036/**
    037 * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
    038 * when closed.
    039 *
    040 * @since 2.0
    041 */
    042public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
    043
    044    private static MBeanServer MBEAN_SERVER;
    045
    046    static {
    047        try {
    048            MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
    049        } catch (final NoClassDefFoundError | Exception ignored) {
    050            // ignore - JMX not available
    051        }
    052    }
    053
    054    /** The pool to which I should return. */
    055    private final ObjectPool<PoolableConnection> pool;
    056
    057    private final ObjectNameWrapper jmxObjectName;
    058
    059    // Use a prepared statement for validation, retaining the last used SQL to
    060    // check if the validation query has changed.
    061    private PreparedStatement validationPreparedStatement;
    062    private String lastValidationSql;
    063
    064    /**
    065     * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
    066     * considered broken and not pass validation in the future.
    067     */
    068    private boolean fatalSqlExceptionThrown;
    069
    070    /**
    071     * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
    072     * {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
    073     */
    074    private final Collection<String> disconnectionSqlCodes;
    075
    076    /** Whether or not to fast fail validation after fatal connection errors */
    077    private final boolean fastFailValidation;
    078
    079    /**
    080     *
    081     * @param conn
    082     *            my underlying connection
    083     * @param pool
    084     *            the pool to which I should return when closed
    085     * @param jmxName
    086     *            JMX name
    087     */
    088    public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
    089            final ObjectName jmxName) {
    090        this(conn, pool, jmxName, null, true);
    091    }
    092
    093    /**
    094     *
    095     * @param conn
    096     *            my underlying connection
    097     * @param pool
    098     *            the pool to which I should return when closed
    099     * @param jmxObjectName
    100     *            JMX name
    101     * @param disconnectSqlCodes
    102     *            SQL_STATE codes considered fatal disconnection errors
    103     * @param fastFailValidation
    104     *            true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
    105     *            run query or isValid)
    106     */
    107    public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
    108            final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
    109            final boolean fastFailValidation) {
    110        super(conn);
    111        this.pool = pool;
    112        this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
    113        this.disconnectionSqlCodes = disconnectSqlCodes;
    114        this.fastFailValidation = fastFailValidation;
    115
    116        if (jmxObjectName != null) {
    117            try {
    118                MBEAN_SERVER.registerMBean(this, jmxObjectName);
    119            } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException ignored) {
    120                // For now, simply skip registration
    121            }
    122        }
    123    }
    124
    125    /**
    126     * Abort my underlying {@link Connection}.
    127     *
    128     * @since 2.9.0
    129     */
    130    @Override
    131    public void abort(final Executor executor) throws SQLException {
    132        if (jmxObjectName != null) {
    133            jmxObjectName.unregisterMBean();
    134        }
    135        super.abort(executor);
    136    }
    137
    138    /**
    139     * Returns me to my pool.
    140     */
    141    @Override
    142    public synchronized void close() throws SQLException {
    143        if (isClosedInternal()) {
    144            // already closed
    145            return;
    146        }
    147
    148        boolean isUnderlyingConnectionClosed;
    149        try {
    150            isUnderlyingConnectionClosed = getDelegateInternal().isClosed();
    151        } catch (final SQLException e) {
    152            try {
    153                pool.invalidateObject(this);
    154            } catch (final IllegalStateException ise) {
    155                // pool is closed, so close the connection
    156                passivate();
    157                getInnermostDelegate().close();
    158            } catch (final Exception ignored) {
    159                // DO NOTHING the original exception will be rethrown
    160            }
    161            throw new SQLException("Cannot close connection (isClosed check failed)", e);
    162        }
    163
    164        /*
    165         * Can't set close before this code block since the connection needs to be open when validation runs. Can't set
    166         * close after this code block since by then the connection will have been returned to the pool and may have
    167         * been borrowed by another thread. Therefore, the close flag is set in passivate().
    168         */
    169        if (isUnderlyingConnectionClosed) {
    170            // Abnormal close: underlying connection closed unexpectedly, so we
    171            // must destroy this proxy
    172            try {
    173                pool.invalidateObject(this);
    174            } catch (final IllegalStateException e) {
    175                // pool is closed, so close the connection
    176                passivate();
    177                getInnermostDelegate().close();
    178            } catch (final Exception e) {
    179                throw new SQLException("Cannot close connection (invalidating pooled object failed)", e);
    180            }
    181        } else {
    182            // Normal close: underlying connection is still open, so we
    183            // simply need to return this proxy to the pool
    184            try {
    185                pool.returnObject(this);
    186            } catch (final IllegalStateException e) {
    187                // pool is closed, so close the connection
    188                passivate();
    189                getInnermostDelegate().close();
    190            } catch (final SQLException | RuntimeException e) {
    191                throw e;
    192            } catch (final Exception e) {
    193                throw new SQLException("Cannot close connection (return to pool failed)", e);
    194            }
    195        }
    196    }
    197
    198    /**
    199     * @return The disconnection SQL codes.
    200     * @since 2.6.0
    201     */
    202    public Collection<String> getDisconnectionSqlCodes() {
    203        return disconnectionSqlCodes;
    204    }
    205
    206    /**
    207     * Expose the {@link #toString()} method via a bean getter, so it can be read as a property via JMX.
    208     */
    209    @Override
    210    public String getToString() {
    211        return toString();
    212    }
    213
    214    @Override
    215    protected void handleException(final SQLException e) throws SQLException {
    216        fatalSqlExceptionThrown |= isFatalException(e);
    217        super.handleException(e);
    218    }
    219
    220    /**
    221     * {@inheritDoc}
    222     * <p>
    223     * This method should not be used by a client to determine whether or not a connection should be return to the
    224     * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool
    225     * once it is no longer required.
    226     */
    227    @Override
    228    public boolean isClosed() throws SQLException {
    229        if (isClosedInternal()) {
    230            return true;
    231        }
    232
    233        if (getDelegateInternal().isClosed()) {
    234            // Something has gone wrong. The underlying connection has been
    235            // closed without the connection being returned to the pool. Return
    236            // it now.
    237            close();
    238            return true;
    239        }
    240
    241        return false;
    242    }
    243
    244    /**
    245     * Checks the SQLState of the input exception.
    246     * <p>
    247     * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the configured list of fatal
    248     * exception codes. If this property is not set, codes are compared against the default codes in
    249     * {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link
    250     * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
    251     * </p>
    252     *
    253     * @param e SQLException to be examined
    254     * @return true if the exception signals a disconnection
    255     */
    256    boolean isDisconnectionSqlException(final SQLException e) {
    257        boolean fatalException = false;
    258        final String sqlState = e.getSQLState();
    259        if (sqlState != null) {
    260            fatalException = disconnectionSqlCodes == null
    261                ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX) || Utils.getDisconnectionSqlCodes().contains(sqlState)
    262                : disconnectionSqlCodes.contains(sqlState);
    263        }
    264        return fatalException;
    265    }
    266
    267    /**
    268     * @return Whether to fail-fast.
    269     * @since 2.6.0
    270     */
    271    public boolean isFastFailValidation() {
    272        return fastFailValidation;
    273    }
    274
    275    /**
    276     * Checks the SQLState of the input exception and any nested SQLExceptions it wraps.
    277     * <p>
    278     * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the
    279     * configured list of fatal exception codes. If this property is not set, codes are compared against the default
    280     * codes in {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link
    281     * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
    282     * </p>
    283     *
    284     * @param e
    285     *            SQLException to be examined
    286     * @return true if the exception signals a disconnection
    287     */
    288    boolean isFatalException(final SQLException e) {
    289        boolean fatalException = isDisconnectionSqlException(e);
    290        if (!fatalException) {
    291            SQLException parentException = e;
    292            SQLException nextException = e.getNextException();
    293            while (nextException != null && nextException != parentException && !fatalException) {
    294                fatalException = isDisconnectionSqlException(nextException);
    295                parentException = nextException;
    296                nextException = parentException.getNextException();
    297            }
    298        }
    299        return fatalException;
    300    }
    301
    302    @Override
    303    protected void passivate() throws SQLException {
    304        super.passivate();
    305        setClosedInternal(true);
    306        if (getDelegateInternal() instanceof PoolingConnection) {
    307            ((PoolingConnection) getDelegateInternal()).connectionReturnedToPool();
    308        }
    309    }
    310
    311    /**
    312     * Actually close my underlying {@link Connection}.
    313     */
    314    @Override
    315    public void reallyClose() throws SQLException {
    316        if (jmxObjectName != null) {
    317            jmxObjectName.unregisterMBean();
    318        }
    319
    320        if (validationPreparedStatement != null) {
    321            Utils.closeQuietly((AutoCloseable) validationPreparedStatement);
    322        }
    323
    324        super.closeInternal();
    325    }
    326
    327    /**
    328     * Validates the connection, using the following algorithm:
    329     * <ol>
    330     * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
    331     * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
    332     * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
    333     * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
    334     * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
    335     * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
    336     * </ol>
    337     *
    338     * @param sql
    339     *            The validation SQL query.
    340     * @param timeoutDuration
    341     *            The validation timeout in seconds.
    342     * @throws SQLException
    343     *             Thrown when validation fails or an SQLException occurs during validation
    344     * @since 2.10.0
    345     */
    346    public void validate(final String sql, Duration timeoutDuration) throws SQLException {
    347        if (fastFailValidation && fatalSqlExceptionThrown) {
    348            throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail"));
    349        }
    350
    351        if (sql == null || sql.isEmpty()) {
    352            if (timeoutDuration.isNegative()) {
    353                timeoutDuration = Duration.ZERO;
    354            }
    355            if (!isValid(timeoutDuration)) {
    356                throw new SQLException("isValid() returned false");
    357            }
    358            return;
    359        }
    360
    361        if (!sql.equals(lastValidationSql)) {
    362            lastValidationSql = sql;
    363            // Has to be the innermost delegate else the prepared statement will
    364            // be closed when the pooled connection is passivated.
    365            validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql);
    366        }
    367
    368        if (timeoutDuration.compareTo(Duration.ZERO) > 0) {
    369            validationPreparedStatement.setQueryTimeout((int) timeoutDuration.getSeconds());
    370        }
    371
    372        try (ResultSet rs = validationPreparedStatement.executeQuery()) {
    373            if (!rs.next()) {
    374                throw new SQLException("validationQuery didn't return a row");
    375            }
    376        } catch (final SQLException sqle) {
    377            throw sqle;
    378        }
    379    }
    380
    381    /**
    382     * Validates the connection, using the following algorithm:
    383     * <ol>
    384     * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
    385     * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
    386     * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
    387     * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
    388     * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
    389     * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
    390     * </ol>
    391     *
    392     * @param sql
    393     *            The validation SQL query.
    394     * @param timeoutSeconds
    395     *            The validation timeout in seconds.
    396     * @throws SQLException
    397     *             Thrown when validation fails or an SQLException occurs during validation
    398     * @deprecated Use {@link #validate(String, Duration)}.
    399     */
    400    @Deprecated
    401    public void validate(final String sql, final int timeoutSeconds) throws SQLException {
    402        validate(sql, Duration.ofSeconds(timeoutSeconds));
    403    }
    404}