001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.dbcp2; 018 019import java.lang.management.ManagementFactory; 020import java.sql.Connection; 021import java.sql.PreparedStatement; 022import java.sql.ResultSet; 023import java.sql.SQLException; 024import java.time.Duration; 025import java.util.Collection; 026import java.util.concurrent.Executor; 027import java.util.concurrent.atomic.AtomicBoolean; 028import java.util.concurrent.locks.Lock; 029import java.util.concurrent.locks.ReentrantLock; 030 031import javax.management.InstanceAlreadyExistsException; 032import javax.management.MBeanRegistrationException; 033import javax.management.MBeanServer; 034import javax.management.NotCompliantMBeanException; 035import javax.management.ObjectName; 036 037import org.apache.commons.pool2.ObjectPool; 038import org.apache.commons.pool2.impl.GenericObjectPool; 039 040/** 041 * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool} 042 * when closed. 043 * 044 * @since 2.0 045 */ 046public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean { 047 048 private static MBeanServer MBEAN_SERVER; 049 050 static { 051 try { 052 MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer(); 053 } catch (final NoClassDefFoundError | Exception ignored) { 054 // ignore - JMX not available 055 } 056 } 057 058 /** The pool to which I should return. */ 059 private final ObjectPool<PoolableConnection> pool; 060 061 private final ObjectNameWrapper jmxObjectName; 062 063 /** 064 * Use a prepared statement for validation, retaining the last used SQL to check if the validation query has changed. 065 */ 066 private PreparedStatement validationPreparedStatement; 067 private String lastValidationSql; 068 069 /** 070 * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be 071 * considered broken and not pass validation in the future. 072 */ 073 private final AtomicBoolean fatalSqlExceptionThrown = new AtomicBoolean(); 074 075 /** 076 * SQL State codes considered to signal fatal conditions. Overrides the defaults in 077 * {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}). 078 */ 079 private final Collection<String> disconnectionSqlCodes; 080 081 /** 082 * A collection of SQL State codes that are not considered fatal disconnection codes. 083 * 084 * @since 2.13.0 085 */ 086 private final Collection<String> disconnectionIgnoreSqlCodes; 087 088 /** Whether or not to fast fail validation after fatal connection errors */ 089 private final boolean fastFailValidation; 090 091 private final Lock lock = new ReentrantLock(); 092 093 /** 094 * Constructs a new instance. 095 * 096 * @param conn 097 * my underlying connection 098 * @param pool 099 * 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}