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 }