View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.dbcp2;
18  
19  import java.lang.management.ManagementFactory;
20  import java.sql.Connection;
21  import java.sql.PreparedStatement;
22  import java.sql.ResultSet;
23  import java.sql.SQLException;
24  import java.time.Duration;
25  import java.util.Collection;
26  import java.util.concurrent.Executor;
27  import java.util.concurrent.locks.Lock;
28  import java.util.concurrent.locks.ReentrantLock;
29  
30  import javax.management.InstanceAlreadyExistsException;
31  import javax.management.MBeanRegistrationException;
32  import javax.management.MBeanServer;
33  import javax.management.NotCompliantMBeanException;
34  import javax.management.ObjectName;
35  
36  import org.apache.commons.pool2.ObjectPool;
37  import org.apache.commons.pool2.impl.GenericObjectPool;
38  
39  /**
40   * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
41   * when closed.
42   *
43   * @since 2.0
44   */
45  public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
46  
47      private static MBeanServer MBEAN_SERVER;
48  
49      static {
50          try {
51              MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
52          } catch (final NoClassDefFoundError | Exception ignored) {
53              // ignore - JMX not available
54          }
55      }
56  
57      /** The pool to which I should return. */
58      private final ObjectPool<PoolableConnection> pool;
59  
60      private final ObjectNameWrapper jmxObjectName;
61  
62      // Use a prepared statement for validation, retaining the last used SQL to
63      // check if the validation query has changed.
64      private PreparedStatement validationPreparedStatement;
65      private String lastValidationSql;
66  
67      /**
68       * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
69       * considered broken and not pass validation in the future.
70       */
71      private boolean fatalSqlExceptionThrown;
72  
73      /**
74       * SQL State codes considered to signal fatal conditions. Overrides the defaults in
75       * {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
76       */
77      private final Collection<String> disconnectionSqlCodes;
78  
79      /**
80       * A collection of SQL State codes that are not considered fatal disconnection codes.
81       *
82       * @since 2.13.0
83       */
84      private final Collection<String> disconnectionIgnoreSqlCodes;
85  
86  
87      /** Whether or not to fast fail validation after fatal connection errors */
88      private final boolean fastFailValidation;
89  
90      private final Lock lock = new ReentrantLock();
91  
92      /**
93       *
94       * @param conn
95       *            my underlying connection
96       * @param pool
97       *            the pool to which I should return when closed
98       * @param jmxName
99       *            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 }