1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.tomcat.dbcp.dbcp2;
18
19 import java.lang.management.ManagementFactory;
20 import java.sql.Connection;
21 import java.sql.PreparedStatement;
22 import java.sql.ResultSet;
23 import java.sql.SQLException;
24 import java.util.Collection;
25
26 import javax.management.InstanceAlreadyExistsException;
27 import javax.management.MBeanRegistrationException;
28 import javax.management.MBeanServer;
29 import javax.management.NotCompliantMBeanException;
30 import javax.management.ObjectName;
31
32 import org.apache.tomcat.dbcp.pool2.ObjectPool;
33
34 /**
35 * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
36 * when closed.
37 *
38 * @since 2.0
39 */
40 public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
41
42 private static MBeanServer MBEAN_SERVER;
43
44 static {
45 try {
46 MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
47 } catch (NoClassDefFoundError | Exception ex) {
48 // ignore - JMX not available
49 }
50 }
51
52 /** The pool to which I should return. */
53 private final ObjectPool<PoolableConnection> pool;
54
55 private final ObjectNameWrapper jmxObjectName;
56
57 // Use a prepared statement for validation, retaining the last used SQL to
58 // check if the validation query has changed.
59 private PreparedStatement validationPreparedStatement;
60 private String lastValidationSql;
61
62 /**
63 * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
64 * considered broken and not pass validation in the future.
65 */
66 private boolean fatalSqlExceptionThrown = false;
67
68 /**
69 * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
70 * {@link Utils#DISCONNECTION_SQL_CODES} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
71 */
72 private final Collection<String> disconnectionSqlCodes;
73
74 /** Whether or not to fast fail validation after fatal connection errors */
75 private final boolean fastFailValidation;
76
77 /**
78 *
79 * @param conn
80 * my underlying connection
81 * @param pool
82 * the pool to which I should return when closed
83 * @param jmxObjectName
84 * JMX name
85 * @param disconnectSqlCodes
86 * SQL_STATE codes considered fatal disconnection errors
87 * @param fastFailValidation
88 * true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
89 * run query or isValid)
90 */
91 public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
92 final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
93 final boolean fastFailValidation) {
94 super(conn);
95 this.pool = pool;
96 this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
97 this.disconnectionSqlCodes = disconnectSqlCodes;
98 this.fastFailValidation = fastFailValidation;
99
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, true);
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 #disconnectionSqlCodes} 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
333 /**
334 * @return The disconnection SQL codes.
335 * @since 2.6.0
336 */
337 public Collection<String> getDisconnectionSqlCodes() {
338 return disconnectionSqlCodes;
339 }
340
341 /**
342 * @return Whether to fail-fast.
343 * @since 2.6.0
344 */
345 public boolean isFastFailValidation() {
346 return fastFailValidation;
347 }
348 }
349