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.util.Collection;
25  import java.util.concurrent.Executor;
26  
27  import javax.management.InstanceAlreadyExistsException;
28  import javax.management.MBeanRegistrationException;
29  import javax.management.MBeanServer;
30  import javax.management.NotCompliantMBeanException;
31  import javax.management.ObjectName;
32  
33  import org.apache.commons.pool2.ObjectPool;
34  
35  /**
36   * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
37   * when closed.
38   *
39   * @since 2.0
40   */
41  public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
42  
43      private static MBeanServer MBEAN_SERVER;
44  
45      static {
46          try {
47              MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
48          } catch (final NoClassDefFoundError | Exception ex) {
49              // ignore - JMX not available
50          }
51      }
52  
53      /** The pool to which I should return. */
54      private final ObjectPool<PoolableConnection> pool;
55  
56      private final ObjectNameWrapper jmxObjectName;
57  
58      // Use a prepared statement for validation, retaining the last used SQL to
59      // check if the validation query has changed.
60      private PreparedStatement validationPreparedStatement;
61      private String lastValidationSql;
62  
63      /**
64       * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
65       * considered broken and not pass validation in the future.
66       */
67      private boolean fatalSqlExceptionThrown;
68  
69      /**
70       * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
71       * {@link Utils#DISCONNECTION_SQL_CODES} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
72       */
73      private final Collection<String> disconnectionSqlCodes;
74  
75      /** Whether or not to fast fail validation after fatal connection errors */
76      private final boolean fastFailValidation;
77  
78      /**
79       *
80       * @param conn
81       *            my underlying connection
82       * @param pool
83       *            the pool to which I should return when closed
84       * @param jmxName
85       *            JMX name
86       */
87      public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
88              final ObjectName jmxName) {
89          this(conn, pool, jmxName, null, true);
90      }
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 jmxObjectName
99       *            JMX name
100      * @param disconnectSqlCodes
101      *            SQL_STATE codes considered fatal disconnection errors
102      * @param fastFailValidation
103      *            true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
104      *            run query or isValid)
105      */
106     public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
107             final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
108             final boolean fastFailValidation) {
109         super(conn);
110         this.pool = pool;
111         this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
112         this.disconnectionSqlCodes = disconnectSqlCodes;
113         this.fastFailValidation = fastFailValidation;
114 
115         if (jmxObjectName != null) {
116             try {
117                 MBEAN_SERVER.registerMBean(this, jmxObjectName);
118             } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
119                 // For now, simply skip registration
120             }
121         }
122     }
123 
124     /**
125      * Abort my underlying {@link Connection}.
126      *
127      * @since 2.9.0
128      */
129     @Override
130     public void abort(final Executor executor) throws SQLException {
131         if (jmxObjectName != null) {
132             jmxObjectName.unregisterMBean();
133         }
134         super.abort(executor);
135     }
136 
137     /**
138      * Returns me to my pool.
139      */
140     @Override
141     public synchronized void close() throws SQLException {
142         if (isClosedInternal()) {
143             // already closed
144             return;
145         }
146 
147         boolean isUnderlyingConnectionClosed;
148         try {
149             isUnderlyingConnectionClosed = getDelegateInternal().isClosed();
150         } catch (final SQLException e) {
151             try {
152                 pool.invalidateObject(this);
153             } catch (final IllegalStateException ise) {
154                 // pool is closed, so close the connection
155                 passivate();
156                 getInnermostDelegate().close();
157             } catch (final Exception ie) {
158                 // DO NOTHING the original exception will be rethrown
159             }
160             throw new SQLException("Cannot close connection (isClosed check failed)", e);
161         }
162 
163         /*
164          * Can't set close before this code block since the connection needs to be open when validation runs. Can't set
165          * close after this code block since by then the connection will have been returned to the pool and may have
166          * been borrowed by another thread. Therefore, the close flag is set in passivate().
167          */
168         if (isUnderlyingConnectionClosed) {
169             // Abnormal close: underlying connection closed unexpectedly, so we
170             // must destroy this proxy
171             try {
172                 pool.invalidateObject(this);
173             } catch (final IllegalStateException e) {
174                 // pool is closed, so close the connection
175                 passivate();
176                 getInnermostDelegate().close();
177             } catch (final Exception e) {
178                 throw new SQLException("Cannot close connection (invalidating pooled object failed)", e);
179             }
180         } else {
181             // Normal close: underlying connection is still open, so we
182             // simply need to return this proxy to the pool
183             try {
184                 pool.returnObject(this);
185             } catch (final IllegalStateException e) {
186                 // pool is closed, so close the connection
187                 passivate();
188                 getInnermostDelegate().close();
189             } catch (final SQLException | RuntimeException e) {
190                 throw e;
191             } catch (final Exception e) {
192                 throw new SQLException("Cannot close connection (return to pool failed)", e);
193             }
194         }
195     }
196 
197     /**
198      * @return The disconnection SQL codes.
199      * @since 2.6.0
200      */
201     public Collection<String> getDisconnectionSqlCodes() {
202         return disconnectionSqlCodes;
203     }
204 
205     /**
206      * Expose the {@link #toString()} method via a bean getter so it can be read as a property via JMX.
207      */
208     @Override
209     public String getToString() {
210         return toString();
211     }
212 
213     @Override
214     protected void handleException(final SQLException e) throws SQLException {
215         fatalSqlExceptionThrown |= isDisconnectionSqlException(e);
216         super.handleException(e);
217     }
218 
219     /**
220      * {@inheritDoc}
221      * <p>
222      * This method should not be used by a client to determine whether or not a connection should be return to the
223      * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool
224      * once it is no longer required.
225      */
226     @Override
227     public boolean isClosed() throws SQLException {
228         if (isClosedInternal()) {
229             return true;
230         }
231 
232         if (getDelegateInternal().isClosed()) {
233             // Something has gone wrong. The underlying connection has been
234             // closed without the connection being returned to the pool. Return
235             // it now.
236             close();
237             return true;
238         }
239 
240         return false;
241     }
242 
243     /**
244      * Checks the SQLState of the input exception and any nested SQLExceptions it wraps.
245      * <p>
246      * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the
247      * configured list of fatal exception codes. If this property is not set, codes are compared against the default
248      * codes in {@link Utils#DISCONNECTION_SQL_CODES} and in this case anything starting with #{link
249      * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
250      * </p>
251      *
252      * @param e
253      *            SQLException to be examined
254      * @return true if the exception signals a disconnection
255      */
256     private boolean isDisconnectionSqlException(final SQLException e) {
257         boolean fatalException = false;
258         final String sqlState = e.getSQLState();
259         if (sqlState != null) {
260             fatalException = disconnectionSqlCodes == null
261                     ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX)
262                             || Utils.DISCONNECTION_SQL_CODES.contains(sqlState)
263                     : disconnectionSqlCodes.contains(sqlState);
264             if (!fatalException) {
265                 final SQLException nextException = e.getNextException();
266                 if (nextException != null && nextException != e) {
267                     fatalException = isDisconnectionSqlException(e.getNextException());
268                 }
269             }
270         }
271         return fatalException;
272     }
273 
274     /**
275      * @return Whether to fail-fast.
276      * @since 2.6.0
277      */
278     public boolean isFastFailValidation() {
279         return fastFailValidation;
280     }
281 
282     @Override
283     protected void passivate() throws SQLException {
284         super.passivate();
285         setClosedInternal(true);
286         if (getDelegateInternal() instanceof PoolingConnection) {
287             ((PoolingConnection) getDelegateInternal()).connectionReturnedToPool();
288         }
289     }
290 
291     /**
292      * Actually close my underlying {@link Connection}.
293      */
294     @Override
295     public void reallyClose() throws SQLException {
296         if (jmxObjectName != null) {
297             jmxObjectName.unregisterMBean();
298         }
299 
300         if (validationPreparedStatement != null) {
301             Utils.closeQuietly(validationPreparedStatement);
302         }
303 
304         super.closeInternal();
305     }
306 
307     /**
308      * Validates the connection, using the following algorithm:
309      * <ol>
310      * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
311      * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
312      * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
313      * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
314      * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
315      * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
316      * </ol>
317      *
318      * @param sql
319      *            The validation SQL query.
320      * @param timeoutSeconds
321      *            The validation timeout in seconds.
322      * @throws SQLException
323      *             Thrown when validation fails or an SQLException occurs during validation
324      */
325     public void validate(final String sql, int timeoutSeconds) throws SQLException {
326         if (fastFailValidation && fatalSqlExceptionThrown) {
327             throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail"));
328         }
329 
330         if (sql == null || sql.isEmpty()) {
331             if (timeoutSeconds < 0) {
332                 timeoutSeconds = 0;
333             }
334             if (!isValid(timeoutSeconds)) {
335                 throw new SQLException("isValid() returned false");
336             }
337             return;
338         }
339 
340         if (!sql.equals(lastValidationSql)) {
341             lastValidationSql = sql;
342             // Has to be the innermost delegate else the prepared statement will
343             // be closed when the pooled connection is passivated.
344             validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql);
345         }
346 
347         if (timeoutSeconds > 0) {
348             validationPreparedStatement.setQueryTimeout(timeoutSeconds);
349         }
350 
351         try (ResultSet rs = validationPreparedStatement.executeQuery()) {
352             if (!rs.next()) {
353                 throw new SQLException("validationQuery didn't return a row");
354             }
355         } catch (final SQLException sqle) {
356             throw sqle;
357         }
358     }
359 }