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}