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 /** Whether or not to fast fail validation after fatal connection errors */ 080 private final boolean fastFailValidation; 081 082 private final Lock lock = new ReentrantLock(); 083 084 /** 085 * 086 * @param conn 087 * my underlying connection 088 * @param pool 089 * the pool to which I should return when closed 090 * @param jmxName 091 * JMX name 092 */ 093 public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool, 094 final ObjectName jmxName) { 095 this(conn, pool, jmxName, null, true); 096 } 097 098 /** 099 * 100 * @param conn 101 * my underlying connection 102 * @param pool 103 * the pool to which I should return when closed 104 * @param jmxObjectName 105 * JMX name 106 * @param disconnectSqlCodes 107 * SQL_STATE codes considered fatal disconnection errors 108 * @param fastFailValidation 109 * true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to 110 * run query or isValid) 111 */ 112 public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool, 113 final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes, 114 final boolean fastFailValidation) { 115 super(conn); 116 this.pool = pool; 117 this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName); 118 this.disconnectionSqlCodes = disconnectSqlCodes; 119 this.fastFailValidation = fastFailValidation; 120 121 if (jmxObjectName != null) { 122 try { 123 MBEAN_SERVER.registerMBean(this, jmxObjectName); 124 } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException ignored) { 125 // For now, simply skip registration 126 } 127 } 128 } 129 130 /** 131 * Abort my underlying {@link Connection}. 132 * 133 * @since 2.9.0 134 */ 135 @Override 136 public void abort(final Executor executor) throws SQLException { 137 if (jmxObjectName != null) { 138 jmxObjectName.unregisterMBean(); 139 } 140 super.abort(executor); 141 } 142 143 /** 144 * Returns me to my pool. 145 */ 146 @Override 147 public void close() throws SQLException { 148 lock.lock(); 149 try { 150 if (isClosedInternal()) { 151 // already closed 152 return; 153 } 154 155 boolean isUnderlyingConnectionClosed; 156 try { 157 isUnderlyingConnectionClosed = getDelegateInternal().isClosed(); 158 } catch (final SQLException e) { 159 try { 160 pool.invalidateObject(this); 161 } catch (final IllegalStateException ise) { 162 // pool is closed, so close the connection 163 passivate(); 164 getInnermostDelegate().close(); 165 } catch (final Exception ignored) { 166 // DO NOTHING the original exception will be rethrown 167 } 168 throw new SQLException("Cannot close connection (isClosed check failed)", e); 169 } 170 171 /* 172 * Can't set close before this code block since the connection needs to be open when validation runs. Can't set 173 * close after this code block since by then the connection will have been returned to the pool and may have 174 * been borrowed by another thread. Therefore, the close flag is set in passivate(). 175 */ 176 if (isUnderlyingConnectionClosed) { 177 // Abnormal close: underlying connection closed unexpectedly, so we 178 // must destroy this proxy 179 try { 180 pool.invalidateObject(this); 181 } catch (final IllegalStateException e) { 182 // pool is closed, so close the connection 183 passivate(); 184 getInnermostDelegate().close(); 185 } catch (final Exception e) { 186 throw new SQLException("Cannot close connection (invalidating pooled object failed)", e); 187 } 188 } else { 189 // Normal close: underlying connection is still open, so we 190 // simply need to return this proxy to the pool 191 try { 192 pool.returnObject(this); 193 } catch (final IllegalStateException e) { 194 // pool is closed, so close the connection 195 passivate(); 196 getInnermostDelegate().close(); 197 } catch (final SQLException | RuntimeException e) { 198 throw e; 199 } catch (final Exception e) { 200 throw new SQLException("Cannot close connection (return to pool failed)", e); 201 } 202 } 203 } finally { 204 lock.unlock(); 205 } 206 } 207 208 /** 209 * @return The disconnection SQL codes. 210 * @since 2.6.0 211 */ 212 public Collection<String> getDisconnectionSqlCodes() { 213 return disconnectionSqlCodes; 214 } 215 216 /** 217 * Expose the {@link #toString()} method via a bean getter, so it can be read as a property via JMX. 218 */ 219 @Override 220 public String getToString() { 221 return toString(); 222 } 223 224 @Override 225 protected void handleException(final SQLException e) throws SQLException { 226 fatalSqlExceptionThrown |= isFatalException(e); 227 super.handleException(e); 228 } 229 230 /** 231 * {@inheritDoc} 232 * <p> 233 * This method should not be used by a client to determine whether or not a connection should be return to the 234 * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool 235 * once it is no longer required. 236 */ 237 @Override 238 public boolean isClosed() throws SQLException { 239 if (isClosedInternal()) { 240 return true; 241 } 242 243 if (getDelegateInternal().isClosed()) { 244 // Something has gone wrong. The underlying connection has been 245 // closed without the connection being returned to the pool. Return 246 // it now. 247 close(); 248 return true; 249 } 250 251 return false; 252 } 253 254 /** 255 * Checks the SQLState of the input exception. 256 * <p> 257 * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the configured list of fatal 258 * exception codes. If this property is not set, codes are compared against the default codes in 259 * {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link 260 * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection. 261 * </p> 262 * 263 * @param e SQLException to be examined 264 * @return true if the exception signals a disconnection 265 */ 266 boolean isDisconnectionSqlException(final SQLException e) { 267 boolean fatalException = false; 268 final String sqlState = e.getSQLState(); 269 if (sqlState != null) { 270 fatalException = disconnectionSqlCodes == null 271 ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX) || Utils.getDisconnectionSqlCodes().contains(sqlState) 272 : disconnectionSqlCodes.contains(sqlState); 273 } 274 return fatalException; 275 } 276 277 /** 278 * @return Whether to fail-fast. 279 * @since 2.6.0 280 */ 281 public boolean isFastFailValidation() { 282 return fastFailValidation; 283 } 284 285 /** 286 * Checks the SQLState of the input exception and any nested SQLExceptions it wraps. 287 * <p> 288 * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the 289 * configured list of fatal exception codes. If this property is not set, codes are compared against the default 290 * codes in {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link 291 * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection. 292 * </p> 293 * 294 * @param e 295 * SQLException to be examined 296 * @return true if the exception signals a disconnection 297 */ 298 boolean isFatalException(final SQLException e) { 299 boolean fatalException = isDisconnectionSqlException(e); 300 if (!fatalException) { 301 SQLException parentException = e; 302 SQLException nextException = e.getNextException(); 303 while (nextException != null && nextException != parentException && !fatalException) { 304 fatalException = isDisconnectionSqlException(nextException); 305 parentException = nextException; 306 nextException = parentException.getNextException(); 307 } 308 } 309 return fatalException; 310 } 311 312 @Override 313 protected void passivate() throws SQLException { 314 super.passivate(); 315 setClosedInternal(true); 316 if (getDelegateInternal() instanceof PoolingConnection) { 317 ((PoolingConnection) getDelegateInternal()).connectionReturnedToPool(); 318 } 319 } 320 321 /** 322 * Actually close my underlying {@link Connection}. 323 */ 324 @Override 325 public void reallyClose() throws SQLException { 326 if (jmxObjectName != null) { 327 jmxObjectName.unregisterMBean(); 328 } 329 330 if (validationPreparedStatement != null) { 331 Utils.closeQuietly((AutoCloseable) validationPreparedStatement); 332 } 333 334 super.closeInternal(); 335 } 336 337 @Override 338 public void setLastUsed() { 339 super.setLastUsed(); 340 if (pool instanceof GenericObjectPool<?>) { 341 final GenericObjectPool<PoolableConnection> gop = (GenericObjectPool<PoolableConnection>) pool; 342 if (gop.isAbandonedConfig()) { 343 gop.use(this); 344 } 345 } 346 } 347 348 /** 349 * Validates the connection, using the following algorithm: 350 * <ol> 351 * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously 352 * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li> 353 * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it 354 * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li> 355 * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at 356 * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li> 357 * </ol> 358 * 359 * @param sql 360 * The validation SQL query. 361 * @param timeoutDuration 362 * The validation timeout in seconds. 363 * @throws SQLException 364 * Thrown when validation fails or an SQLException occurs during validation 365 * @since 2.10.0 366 */ 367 public void validate(final String sql, Duration timeoutDuration) throws SQLException { 368 if (fastFailValidation && fatalSqlExceptionThrown) { 369 throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail")); 370 } 371 372 if (sql == null || sql.isEmpty()) { 373 if (timeoutDuration.isNegative()) { 374 timeoutDuration = Duration.ZERO; 375 } 376 if (!isValid(timeoutDuration)) { 377 throw new SQLException("isValid() returned false"); 378 } 379 return; 380 } 381 382 if (!sql.equals(lastValidationSql)) { 383 lastValidationSql = sql; 384 // Has to be the innermost delegate else the prepared statement will 385 // be closed when the pooled connection is passivated. 386 validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql); 387 } 388 389 if (timeoutDuration.compareTo(Duration.ZERO) > 0) { 390 validationPreparedStatement.setQueryTimeout((int) timeoutDuration.getSeconds()); 391 } 392 393 try (ResultSet rs = validationPreparedStatement.executeQuery()) { 394 if (!rs.next()) { 395 throw new SQLException("validationQuery didn't return a row"); 396 } 397 } catch (final SQLException sqle) { 398 throw sqle; 399 } 400 } 401 402 /** 403 * Validates the connection, using the following algorithm: 404 * <ol> 405 * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously 406 * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li> 407 * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it 408 * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li> 409 * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at 410 * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li> 411 * </ol> 412 * 413 * @param sql 414 * The validation SQL query. 415 * @param timeoutSeconds 416 * The validation timeout in seconds. 417 * @throws SQLException 418 * Thrown when validation fails or an SQLException occurs during validation 419 * @deprecated Use {@link #validate(String, Duration)}. 420 */ 421 @Deprecated 422 public void validate(final String sql, final int timeoutSeconds) throws SQLException { 423 validate(sql, Duration.ofSeconds(timeoutSeconds)); 424 } 425}