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 }