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      /** Whether or not to fast fail validation after fatal connection errors */
80      private final boolean fastFailValidation;
81  
82      private final Lock lock = new ReentrantLock();
83  
84      /**
85       *
86       * @param conn
87       *            my underlying connection
88       * @param pool
89       *            the pool to which I should return when closed
90       * @param jmxName
91       *            JMX name
92       */
93      public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
94              final ObjectName jmxName) {
95          this(conn, pool, jmxName, null, true);
96      }
97  
98      /**
99       *
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 }