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}