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