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