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