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