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     * @return Whether to fail-fast.
269     * @since 2.6.0
270     */
271    public boolean isFastFailValidation() {
272        return fastFailValidation;
273    }
274
275    /**
276     * Checks the SQLState of the input exception and any nested SQLExceptions it wraps.
277     * <p>
278     * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the
279     * configured list of fatal exception codes. If this property is not set, codes are compared against the default
280     * codes in {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link
281     * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
282     * </p>
283     *
284     * @param e
285     *            SQLException to be examined
286     * @return true if the exception signals a disconnection
287     */
288    boolean isFatalException(final SQLException e) {
289        boolean fatalException = isDisconnectionSqlException(e);
290        if (!fatalException) {
291            SQLException parentException = e;
292            SQLException nextException = e.getNextException();
293            while (nextException != null && nextException != parentException && !fatalException) {
294                fatalException = isDisconnectionSqlException(nextException);
295                parentException = nextException;
296                nextException = parentException.getNextException();
297            }
298        }
299        return fatalException;
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 timeoutDuration
341     *            The validation timeout in seconds.
342     * @throws SQLException
343     *             Thrown when validation fails or an SQLException occurs during validation
344     * @since 2.10.0
345     */
346    public void validate(final String sql, Duration timeoutDuration) throws SQLException {
347        if (fastFailValidation && fatalSqlExceptionThrown) {
348            throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail"));
349        }
350
351        if (sql == null || sql.isEmpty()) {
352            if (timeoutDuration.isNegative()) {
353                timeoutDuration = Duration.ZERO;
354            }
355            if (!isValid(timeoutDuration)) {
356                throw new SQLException("isValid() returned false");
357            }
358            return;
359        }
360
361        if (!sql.equals(lastValidationSql)) {
362            lastValidationSql = sql;
363            // Has to be the innermost delegate else the prepared statement will
364            // be closed when the pooled connection is passivated.
365            validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql);
366        }
367
368        if (timeoutDuration.compareTo(Duration.ZERO) > 0) {
369            validationPreparedStatement.setQueryTimeout((int) timeoutDuration.getSeconds());
370        }
371
372        try (ResultSet rs = validationPreparedStatement.executeQuery()) {
373            if (!rs.next()) {
374                throw new SQLException("validationQuery didn't return a row");
375            }
376        } catch (final SQLException sqle) {
377            throw sqle;
378        }
379    }
380
381    /**
382     * Validates the connection, using the following algorithm:
383     * <ol>
384     * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
385     * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
386     * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
387     * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
388     * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
389     * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
390     * </ol>
391     *
392     * @param sql
393     *            The validation SQL query.
394     * @param timeoutSeconds
395     *            The validation timeout in seconds.
396     * @throws SQLException
397     *             Thrown when validation fails or an SQLException occurs during validation
398     * @deprecated Use {@link #validate(String, Duration)}.
399     */
400    @Deprecated
401    public void validate(final String sql, final int timeoutSeconds) throws SQLException {
402        validate(sql, Duration.ofSeconds(timeoutSeconds));
403    }
404}