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    *      https://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.atomic.AtomicBoolean;
28  import java.util.concurrent.locks.Lock;
29  import java.util.concurrent.locks.ReentrantLock;
30  
31  import javax.management.InstanceAlreadyExistsException;
32  import javax.management.MBeanRegistrationException;
33  import javax.management.MBeanServer;
34  import javax.management.NotCompliantMBeanException;
35  import javax.management.ObjectName;
36  
37  import org.apache.commons.pool2.ObjectPool;
38  import org.apache.commons.pool2.impl.GenericObjectPool;
39  
40  /**
41   * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
42   * when closed.
43   *
44   * @since 2.0
45   */
46  public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
47  
48      private static MBeanServer MBEAN_SERVER;
49  
50      static {
51          try {
52              MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
53          } catch (final NoClassDefFoundError | Exception ignored) {
54              // ignore - JMX not available
55          }
56      }
57  
58      /** The pool to which I should return. */
59      private final ObjectPool<PoolableConnection> pool;
60  
61      private final ObjectNameWrapper jmxObjectName;
62  
63      /**
64       * Use a prepared statement for validation, retaining the last used SQL to check if the validation query has changed.
65       */
66      private PreparedStatement validationPreparedStatement;
67      private String lastValidationSql;
68  
69      /**
70       * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
71       * considered broken and not pass validation in the future.
72       */
73      private final AtomicBoolean fatalSqlExceptionThrown = new AtomicBoolean();
74  
75      /**
76       * SQL State codes considered to signal fatal conditions. Overrides the defaults in
77       * {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
78       */
79      private final Collection<String> disconnectionSqlCodes;
80  
81      /**
82       * A collection of SQL State codes that are not considered fatal disconnection codes.
83       *
84       * @since 2.13.0
85       */
86      private final Collection<String> disconnectionIgnoreSqlCodes;
87  
88      /** Whether or not to fast fail validation after fatal connection errors */
89      private final boolean fastFailValidation;
90  
91      private final Lock lock = new ReentrantLock();
92  
93      /**
94       * Constructs a new instance.
95       *
96       * @param conn
97       *            my underlying connection
98       * @param pool
99       *            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 }