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