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}