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 * http://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.locks.Lock; 028import java.util.concurrent.locks.ReentrantLock; 029 030import javax.management.InstanceAlreadyExistsException; 031import javax.management.MBeanRegistrationException; 032import javax.management.MBeanServer; 033import javax.management.NotCompliantMBeanException; 034import javax.management.ObjectName; 035 036import org.apache.commons.pool2.ObjectPool; 037import org.apache.commons.pool2.impl.GenericObjectPool; 038 039/** 040 * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool} 041 * when closed. 042 * 043 * @since 2.0 044 */ 045public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean { 046 047 private static MBeanServer MBEAN_SERVER; 048 049 static { 050 try { 051 MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer(); 052 } catch (final NoClassDefFoundError | Exception ignored) { 053 // ignore - JMX not available 054 } 055 } 056 057 /** The pool to which I should return. */ 058 private final ObjectPool<PoolableConnection> pool; 059 060 private final ObjectNameWrapper jmxObjectName; 061 062 // Use a prepared statement for validation, retaining the last used SQL to 063 // check if the validation query has changed. 064 private PreparedStatement validationPreparedStatement; 065 private String lastValidationSql; 066 067 /** 068 * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be 069 * considered broken and not pass validation in the future. 070 */ 071 private boolean fatalSqlExceptionThrown; 072 073 /** 074 * SQL State codes considered to signal fatal conditions. Overrides the defaults in 075 * {@link Utils#getDisconnectionSqlCodes()} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}). 076 */ 077 private final Collection<String> disconnectionSqlCodes; 078 079 /** 080 * A collection of SQL State codes that are not considered fatal disconnection codes. 081 * 082 * @since 2.13.0 083 */ 084 private final Collection<String> disconnectionIgnoreSqlCodes; 085 086 087 /** Whether or not to fast fail validation after fatal connection errors */ 088 private final boolean fastFailValidation; 089 090 private final Lock lock = new ReentrantLock(); 091 092 /** 093 * 094 * @param conn 095 * my underlying connection 096 * @param pool 097 * the pool to which I should return when closed 098 * @param jmxName 099 * 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}