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}