This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-dbcp.git
The following commit(s) were added to refs/heads/master by this push: new 25aa107a Add support for ignoring non-fatal SQL state codes. (#421) 25aa107a is described below commit 25aa107a8fd349173dc3ce8727e33215fba29e5c Author: Johno Crawford <jo...@hellface.com> AuthorDate: Mon Aug 19 18:17:03 2024 +0200 Add support for ignoring non-fatal SQL state codes. (#421) * Add support for ignoring non-fatal SQL state codes. * Correct since tags. Fix javadocs. Add disconnectionIgnoreSqlCodes to xdoc. Throw illegal argument exception if sql state code configurations overlap. * More tests. * Check style fixes. --- .../org/apache/commons/dbcp2/BasicDataSource.java | 70 ++++++++++++++++++++++ .../commons/dbcp2/BasicDataSourceFactory.java | 17 +++++- .../org/apache/commons/dbcp2/DataSourceMXBean.java | 10 ++++ .../apache/commons/dbcp2/PoolableConnection.java | 40 ++++++++++++- .../commons/dbcp2/PoolableConnectionFactory.java | 38 +++++++++++- src/main/java/org/apache/commons/dbcp2/Utils.java | 23 +++++++ .../dbcp2/managed/BasicManagedDataSource.java | 1 + .../dbcp2/managed/PoolableManagedConnection.java | 26 +++++++- .../managed/PoolableManagedConnectionFactory.java | 2 +- src/site/xdoc/configuration.xml | 8 +++ .../apache/commons/dbcp2/TestBasicDataSource.java | 46 ++++++++++++++ .../commons/dbcp2/TestBasicDataSourceMXBean.java | 5 ++ .../commons/dbcp2/TestPoolableConnection.java | 22 +++++++ .../java/org/apache/commons/dbcp2/TestUtils.java | 58 ++++++++++++++++++ 14 files changed, 359 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java b/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java index 95b756bf..b7e3fe51 100644 --- a/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java +++ b/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java @@ -16,6 +16,8 @@ */ package org.apache.commons.dbcp2; +import static org.apache.commons.dbcp2.Utils.checkForConflicts; + import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; @@ -346,6 +348,12 @@ public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBean private volatile Set<String> disconnectionSqlCodes; + /** + * A collection of SQL State codes that are not considered fatal disconnection codes. + * @since 2.13.0 + */ + private volatile Set<String> disconnectionIgnoreSqlCodes; + private boolean fastFailValidation; /** @@ -629,6 +637,7 @@ public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBean connectionFactory.setDefaultQueryTimeout(getDefaultQueryTimeoutDuration()); connectionFactory.setFastFailValidation(fastFailValidation); connectionFactory.setDisconnectionSqlCodes(disconnectionSqlCodes); + connectionFactory.setDisconnectionIgnoreSqlCodes(disconnectionIgnoreSqlCodes); validateConnectionFactory(connectionFactory); } catch (final RuntimeException e) { throw e; @@ -864,6 +873,22 @@ public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBean return result == null ? Collections.emptySet() : result; } + /** + * Gets the set of SQL State codes that are not considered fatal disconnection codes. + * <p> + * This method returns the set of SQL State codes that have been specified to be ignored + * when determining if a {@link SQLException} signals a disconnection. These codes will not + * trigger a disconnection even if they match other disconnection criteria. + * </p> + * + * @return a set of SQL State codes that should be ignored for disconnection checks, or an empty set if none have been specified. + * @since 2.13.0 + */ + public Set<String> getDisconnectionIgnoreSqlCodes() { + final Set<String> result = disconnectionIgnoreSqlCodes; + return result == null ? Collections.emptySet() : result; + } + /** * Provides the same data as {@link #getDisconnectionSqlCodes} but in an array so it is accessible via JMX. * @@ -874,6 +899,16 @@ public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBean return getDisconnectionSqlCodes().toArray(Utils.EMPTY_STRING_ARRAY); } + /** + * Provides the same data as {@link #getDisconnectionIgnoreSqlCodes()} but in an array, so it is accessible via JMX. + * + * @since 2.13.0 + */ + @Override + public String[] getDisconnectionIgnoreSqlCodesAsArray() { + return getDisconnectionIgnoreSqlCodes().toArray(Utils.EMPTY_STRING_ARRAY); + } + /** * Gets the JDBC Driver that has been configured for use by this pool. * <p> @@ -954,6 +989,7 @@ public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBean * * @return true if connections created by this datasource will fast fail validation. * @see #setDisconnectionSqlCodes(Collection) + * @see #setDisconnectionIgnoreSqlCodes(Collection) * @since 2.1 */ @Override @@ -1964,13 +2000,47 @@ public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBean * * @param disconnectionSqlCodes SQL State codes considered to signal fatal conditions * @since 2.1 + * @throws IllegalArgumentException if any SQL state codes overlap with those in {@link #disconnectionIgnoreSqlCodes}. */ public void setDisconnectionSqlCodes(final Collection<String> disconnectionSqlCodes) { + checkForConflicts(disconnectionSqlCodes, this.disconnectionIgnoreSqlCodes, + "disconnectionSqlCodes", "disconnectionIgnoreSqlCodes"); final Set<String> collect = Utils.isEmpty(disconnectionSqlCodes) ? null : disconnectionSqlCodes.stream().filter(s -> !isEmpty(s)).collect(Collectors.toSet()); this.disconnectionSqlCodes = Utils.isEmpty(collect) ? null : collect; } + /** + * Sets the SQL State codes that should be ignored when determining fatal disconnection conditions. + * <p> + * This method allows you to specify a collection of SQL State codes that will be excluded from + * disconnection checks. These codes will not trigger the "fatally disconnected" status even if they + * match the typical disconnection criteria. This can be useful in scenarios where certain SQL State + * codes (e.g., specific codes starting with "08") are known to be non-fatal in your environment. + * </p> + * <p> + * The effect of this method is similar to the one described in {@link #setDisconnectionSqlCodes(Collection)}, + * but instead of setting codes that signal fatal disconnections, it defines codes that should be ignored + * during such checks. + * </p> + * <p> + * Note: This method currently has no effect once the pool has been initialized. The pool is initialized the first + * time one of the following methods is invoked: {@code getConnection, setLogwriter, setLoginTimeout, + * getLoginTimeout, getLogWriter}. + * </p> + * + * @param disconnectionIgnoreSqlCodes SQL State codes that should be ignored in disconnection checks + * @since 2.13.0 + * @throws IllegalArgumentException if any SQL state codes overlap with those in {@link #disconnectionSqlCodes}. + */ + public void setDisconnectionIgnoreSqlCodes(final Collection<String> disconnectionIgnoreSqlCodes) { + checkForConflicts(disconnectionIgnoreSqlCodes, this.disconnectionSqlCodes, + "disconnectionIgnoreSqlCodes", "disconnectionSqlCodes"); + final Set<String> collect = Utils.isEmpty(disconnectionIgnoreSqlCodes) ? null + : disconnectionIgnoreSqlCodes.stream().filter(s -> !isEmpty(s)).collect(Collectors.toSet()); + this.disconnectionIgnoreSqlCodes = Utils.isEmpty(collect) ? null : collect; + } + /** * Sets the JDBC Driver instance to use for this pool. * <p> diff --git a/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java b/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java index a28b21bf..84beb588 100644 --- a/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java +++ b/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java @@ -120,6 +120,18 @@ public class BasicDataSourceFactory implements ObjectFactory { */ private static final String PROP_DISCONNECTION_SQL_CODES = "disconnectionSqlCodes"; + /** + * Property key for specifying the SQL State codes that should be ignored during disconnection checks. + * <p> + * The value for this property must be a comma-separated string of SQL State codes, where each code represents + * a state that will be excluded from being treated as a fatal disconnection. The expected format is a series + * of SQL State codes separated by commas, with no spaces between them (e.g., "08003,08004"). + * </p> + * @since 2.13.0 + */ + private static final String PROP_DISCONNECTION_IGNORE_SQL_CODES = "disconnectionIgnoreSqlCodes"; + + /* * Block with obsolete properties from DBCP 1.x. Warn users that these are ignored and they should use the 2.x * properties. @@ -149,8 +161,8 @@ public class BasicDataSourceFactory implements ObjectFactory { PROP_CLEAR_STATEMENT_POOL_ON_RETURN, PROP_MAX_OPEN_PREPARED_STATEMENTS, PROP_CONNECTION_PROPERTIES, PROP_MAX_CONN_LIFETIME_MILLIS, PROP_LOG_EXPIRED_CONNECTIONS, PROP_ROLLBACK_ON_RETURN, PROP_ENABLE_AUTO_COMMIT_ON_RETURN, - PROP_DEFAULT_QUERY_TIMEOUT, PROP_FAST_FAIL_VALIDATION, PROP_DISCONNECTION_SQL_CODES, PROP_JMX_NAME, - PROP_REGISTER_CONNECTION_MBEAN, PROP_CONNECTION_FACTORY_CLASS_NAME); + PROP_DEFAULT_QUERY_TIMEOUT, PROP_FAST_FAIL_VALIDATION, PROP_DISCONNECTION_SQL_CODES, PROP_DISCONNECTION_IGNORE_SQL_CODES, + PROP_JMX_NAME, PROP_REGISTER_CONNECTION_MBEAN, PROP_CONNECTION_FACTORY_CLASS_NAME); /** * Obsolete properties from DBCP 1.x. with warning strings suggesting new properties. LinkedHashMap will guarantee @@ -302,6 +314,7 @@ public class BasicDataSourceFactory implements ObjectFactory { acceptDurationOfSeconds(properties, PROP_DEFAULT_QUERY_TIMEOUT, dataSource::setDefaultQueryTimeout); acceptBoolean(properties, PROP_FAST_FAIL_VALIDATION, dataSource::setFastFailValidation); getOptional(properties, PROP_DISCONNECTION_SQL_CODES).ifPresent(v -> dataSource.setDisconnectionSqlCodes(parseList(v, ','))); + getOptional(properties, PROP_DISCONNECTION_IGNORE_SQL_CODES).ifPresent(v -> dataSource.setDisconnectionIgnoreSqlCodes(parseList(v, ','))); acceptString(properties, PROP_CONNECTION_FACTORY_CLASS_NAME, dataSource::setConnectionFactoryClassName); // DBCP-215 diff --git a/src/main/java/org/apache/commons/dbcp2/DataSourceMXBean.java b/src/main/java/org/apache/commons/dbcp2/DataSourceMXBean.java index d3208c20..3c55187b 100644 --- a/src/main/java/org/apache/commons/dbcp2/DataSourceMXBean.java +++ b/src/main/java/org/apache/commons/dbcp2/DataSourceMXBean.java @@ -93,6 +93,16 @@ public interface DataSourceMXBean { */ String[] getDisconnectionSqlCodesAsArray(); + /** + * See {@link BasicDataSource#getDisconnectionIgnoreSqlCodesAsArray()}. + * + * @return {@link BasicDataSource#getDisconnectionIgnoreSqlCodesAsArray()}. + * @since 2.13.0 + */ + default String[] getDisconnectionIgnoreSqlCodesAsArray() { + return Utils.EMPTY_STRING_ARRAY; + } + /** * See {@link BasicDataSource#getDriverClassName()}. * diff --git a/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java b/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java index 1497e9e6..26940221 100644 --- a/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java +++ b/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java @@ -76,6 +76,13 @@ public class PoolableConnection extends DelegatingConnection<Connection> impleme */ private final Collection<String> disconnectionSqlCodes; + /** + * A collection of SQL State codes that are not considered fatal disconnection codes. + * @since 2.13.0 + */ + private final Collection<String> disconnectionIgnoreSqlCodes; + + /** Whether or not to fast fail validation after fatal connection errors */ private final boolean fastFailValidation; @@ -109,13 +116,38 @@ public class PoolableConnection extends DelegatingConnection<Connection> impleme * true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to * run query or isValid) */ + public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool, + final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes, + final boolean fastFailValidation) { + this(conn, pool, jmxObjectName, disconnectSqlCodes, null, fastFailValidation); + } + + /** + * Creates a new {@link PoolableConnection} instance. + * + * @param conn + * my underlying connection + * @param pool + * the pool to which I should return when closed + * @param jmxObjectName + * JMX name + * @param disconnectSqlCodes + * SQL State codes considered fatal disconnection errors + * @param disconnectionIgnoreSqlCodes + * SQL State codes that should be ignored when determining fatal disconnection errors + * @param fastFailValidation + * true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to + * run query or isValid) + * @since 2.13.0 + */ public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool, final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes, - final boolean fastFailValidation) { + final Collection<String> disconnectionIgnoreSqlCodes, final boolean fastFailValidation) { super(conn); this.pool = pool; this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName); this.disconnectionSqlCodes = disconnectSqlCodes; + this.disconnectionIgnoreSqlCodes = disconnectionIgnoreSqlCodes; this.fastFailValidation = fastFailValidation; if (jmxObjectName != null) { @@ -257,7 +289,8 @@ public class PoolableConnection extends DelegatingConnection<Connection> impleme * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the configured list of fatal * exception codes. If this property is not set, codes are compared against the default codes in * {@link Utils#getDisconnectionSqlCodes()} and in this case anything starting with #{link - * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection. + * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection. Additionally, any SQL state + * listed in {@link #disconnectionIgnoreSqlCodes} will be ignored and not treated as fatal. * </p> * * @param e SQLException to be examined @@ -267,6 +300,9 @@ public class PoolableConnection extends DelegatingConnection<Connection> impleme boolean fatalException = false; final String sqlState = e.getSQLState(); if (sqlState != null) { + if (disconnectionIgnoreSqlCodes != null && disconnectionIgnoreSqlCodes.contains(sqlState)) { + return false; + } fatalException = disconnectionSqlCodes == null ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX) || Utils.getDisconnectionSqlCodes().contains(sqlState) : disconnectionSqlCodes.contains(sqlState); diff --git a/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java b/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java index 8de3a636..ff6d0513 100644 --- a/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java +++ b/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java @@ -16,6 +16,8 @@ */ package org.apache.commons.dbcp2; +import static org.apache.commons.dbcp2.Utils.checkForConflicts; + import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; @@ -64,6 +66,8 @@ public class PoolableConnectionFactory implements PooledObjectFactory<PoolableCo private Collection<String> disconnectionSqlCodes; + private Collection<String> disconnectionIgnoreSqlCodes; + private boolean fastFailValidation = true; private volatile ObjectPool<PoolableConnection> pool; @@ -290,6 +294,21 @@ public class PoolableConnectionFactory implements PooledObjectFactory<PoolableCo return disconnectionSqlCodes; } + /** + * Retrieves the collection of SQL State codes that are not considered fatal disconnection codes. + * <p> + * This method returns the collection of SQL State codes that have been set to be ignored when + * determining if a {@link SQLException} signals a disconnection. These codes are excluded from + * being treated as fatal even if they match the typical disconnection criteria. + * </p> + * + * @return a {@link Collection} of SQL State codes that should be ignored for disconnection checks. + * @since 2.13.0 + */ + public Collection<String> getDisconnectionIgnoreSqlCodes() { + return disconnectionIgnoreSqlCodes; + } + /** * Gets the Maximum connection duration. * @@ -464,7 +483,8 @@ public class PoolableConnectionFactory implements PooledObjectFactory<PoolableCo } } - final PoolableConnection pc = new PoolableConnection(conn, pool, connJmxName, disconnectionSqlCodes, fastFailValidation); + final PoolableConnection pc = new PoolableConnection(conn, pool, connJmxName, + disconnectionSqlCodes, disconnectionIgnoreSqlCodes, fastFailValidation); pc.setCacheState(cacheState); return new DefaultPooledObject<>(pc); @@ -606,11 +626,27 @@ public class PoolableConnectionFactory implements PooledObjectFactory<PoolableCo * The disconnection SQL codes. * @see #getDisconnectionSqlCodes() * @since 2.1 + * @throws IllegalArgumentException if any SQL state codes overlap with those in {@link #disconnectionIgnoreSqlCodes}. */ public void setDisconnectionSqlCodes(final Collection<String> disconnectionSqlCodes) { + checkForConflicts(disconnectionSqlCodes, this.disconnectionIgnoreSqlCodes, + "disconnectionSqlCodes", "disconnectionIgnoreSqlCodes"); this.disconnectionSqlCodes = disconnectionSqlCodes; } + /** + * @param disconnectionIgnoreSqlCodes + * The collection of SQL State codes to be ignored. + * @see #getDisconnectionIgnoreSqlCodes() + * @since 2.13.0 + * @throws IllegalArgumentException if any SQL state codes overlap with those in {@link #disconnectionSqlCodes}. + */ + public void setDisconnectionIgnoreSqlCodes(Collection<String> disconnectionIgnoreSqlCodes) { + checkForConflicts(disconnectionIgnoreSqlCodes, this.disconnectionSqlCodes, + "disconnectionIgnoreSqlCodes", "disconnectionSqlCodes"); + this.disconnectionIgnoreSqlCodes = disconnectionIgnoreSqlCodes; + } + /** * @param autoCommitOnReturn Whether to auto-commit on return. * @deprecated Use {@link #setAutoCommitOnReturn(boolean)}. diff --git a/src/main/java/org/apache/commons/dbcp2/Utils.java b/src/main/java/org/apache/commons/dbcp2/Utils.java index fd106d89..ad927225 100644 --- a/src/main/java/org/apache/commons/dbcp2/Utils.java +++ b/src/main/java/org/apache/commons/dbcp2/Utils.java @@ -138,6 +138,29 @@ public final class Utils { close(autoCloseable, null); } + /** + * Checks for conflicts between two collections. + * <p> + * If any overlap is found between the two provided collections, an {@link IllegalArgumentException} is thrown. + * </p> + * + * @param codes1 The first collection of SQL state codes. + * @param codes2 The second collection of SQL state codes. + * @param name1 The name of the first collection (for exception message). + * @param name2 The name of the second collection (for exception message). + * @since 2.13.0 + * @throws IllegalArgumentException if any codes overlap between the two collections. + */ + static void checkForConflicts(Collection<String> codes1, Collection<String> codes2, String name1, String name2) { + if (codes1 != null && codes2 != null) { + for (String code : codes1) { + if (codes2.contains(code)) { + throw new IllegalArgumentException(code + " cannot be in both " + name1 + " and " + name2 + "."); + } + } + } + } + /** * Closes the Connection (which may be null). * diff --git a/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java b/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java index 719ee4d0..c293e27c 100644 --- a/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java +++ b/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java @@ -146,6 +146,7 @@ public class BasicManagedDataSource extends BasicDataSource { connectionFactory.setDefaultQueryTimeout(getDefaultQueryTimeoutDuration()); connectionFactory.setFastFailValidation(getFastFailValidation()); connectionFactory.setDisconnectionSqlCodes(getDisconnectionSqlCodes()); + connectionFactory.setDisconnectionIgnoreSqlCodes(getDisconnectionIgnoreSqlCodes()); validateConnectionFactory(connectionFactory); } catch (final RuntimeException e) { throw e; diff --git a/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnection.java b/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnection.java index 29d0585b..5eea0397 100644 --- a/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnection.java +++ b/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnection.java @@ -65,7 +65,31 @@ public class PoolableManagedConnection extends PoolableConnection { public PoolableManagedConnection(final TransactionRegistry transactionRegistry, final Connection conn, final ObjectPool<PoolableConnection> pool, final Collection<String> disconnectSqlCodes, final boolean fastFailValidation) { - super(conn, pool, null, disconnectSqlCodes, fastFailValidation); + this(transactionRegistry, conn, pool, disconnectSqlCodes, null, fastFailValidation); + } + + /** + * Create a PoolableManagedConnection. + * + * @param transactionRegistry + * transaction registry + * @param conn + * underlying connection + * @param pool + * connection pool + * @param disconnectSqlCodes + * SQL State codes considered fatal disconnection errors + * @param disconnectionIgnoreSqlCodes + * SQL State codes considered fatal disconnection errors + * @param fastFailValidation + * true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to + * run query or isValid) + * @since 2.13.0 + */ + public PoolableManagedConnection(final TransactionRegistry transactionRegistry, final Connection conn, + final ObjectPool<PoolableConnection> pool, final Collection<String> disconnectSqlCodes, + final Collection<String> disconnectionIgnoreSqlCodes, final boolean fastFailValidation) { + super(conn, pool, null, disconnectSqlCodes, disconnectionIgnoreSqlCodes, fastFailValidation); this.transactionRegistry = transactionRegistry; } diff --git a/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnectionFactory.java b/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnectionFactory.java index 7eb81015..92db944f 100644 --- a/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnectionFactory.java +++ b/src/main/java/org/apache/commons/dbcp2/managed/PoolableManagedConnectionFactory.java @@ -104,7 +104,7 @@ public class PoolableManagedConnectionFactory extends PoolableConnectionFactory ((PoolingConnection) conn).setCacheState(getCacheState()); } final PoolableManagedConnection pmc = new PoolableManagedConnection(transactionRegistry, conn, getPool(), - getDisconnectionSqlCodes(), isFastFailValidation()); + getDisconnectionSqlCodes(), getDisconnectionIgnoreSqlCodes(), isFastFailValidation()); pmc.setCacheState(getCacheState()); return new DefaultPooledObject<>(pmc); } diff --git a/src/site/xdoc/configuration.xml b/src/site/xdoc/configuration.xml index 95797365..9f6ce900 100644 --- a/src/site/xdoc/configuration.xml +++ b/src/site/xdoc/configuration.xml @@ -498,6 +498,14 @@ the parent connection. <code>fastFailValidation</code> is set to <code>true.</code> </td> </tr> +<tr> + <td>disconnectionIgnoreSqlCodes</td> + <td>null</td> + <td>Comma-delimited list of SQL State codes that should be ignored when determining fatal disconnection errors. + These codes will not trigger a fatal disconnection status, even if they match the usual criteria. + Setting this property has no effect unless <code>fastFailValidation</code> is set to <code>true.</code> + </td> +</tr> <tr> <td>jmxName</td> <td></td> diff --git a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSource.java b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSource.java index 47a6d580..3b3ec7ed 100644 --- a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSource.java +++ b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSource.java @@ -36,6 +36,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -459,6 +460,51 @@ public class TestBasicDataSource extends TestConnectionPool { } } + @Test + public void testOverlapBetweenDisconnectionAndIgnoreSqlCodes() { + // Set initial disconnection SQL codes + final HashSet<String> disconnectionSqlCodes = new HashSet<>(Arrays.asList("XXX", "ZZZ")); + ds.setDisconnectionSqlCodes(disconnectionSqlCodes); + + // Try setting ignore SQL codes with overlap + final HashSet<String> disconnectionIgnoreSqlCodes = new HashSet<>(Arrays.asList("YYY", "XXX")); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ds.setDisconnectionIgnoreSqlCodes(disconnectionIgnoreSqlCodes); + }); + + assertEquals("XXX cannot be in both disconnectionIgnoreSqlCodes and disconnectionSqlCodes.", exception.getMessage()); + } + + @Test + public void testNoOverlapBetweenDisconnectionAndIgnoreSqlCodes() { + // Set disconnection SQL codes without overlap + final HashSet<String> disconnectionSqlCodes = new HashSet<>(Arrays.asList("XXX", "ZZZ")); + ds.setDisconnectionSqlCodes(disconnectionSqlCodes); + + // Set ignore SQL codes without overlap + final HashSet<String> disconnectionIgnoreSqlCodes = new HashSet<>(Arrays.asList("YYY", "AAA")); + ds.setDisconnectionIgnoreSqlCodes(disconnectionIgnoreSqlCodes); + + assertEquals(disconnectionSqlCodes, ds.getDisconnectionSqlCodes(), "Disconnection SQL codes should match the set values."); + assertEquals(disconnectionIgnoreSqlCodes, ds.getDisconnectionIgnoreSqlCodes(), "Disconnection Ignore SQL codes should match the set values."); + } + + @Test + public void testDisconnectionIgnoreSqlCodes() throws Exception { + final ArrayList<String> disconnectionIgnoreSqlCodes = new ArrayList<>(); + disconnectionIgnoreSqlCodes.add("XXXX"); + ds.setDisconnectionIgnoreSqlCodes(disconnectionIgnoreSqlCodes); + ds.setFastFailValidation(true); + try (Connection conn = ds.getConnection()) { // Triggers initialization - pcf creation + // Make sure factory got the properties + final PoolableConnectionFactory pcf = (PoolableConnectionFactory) ds.getConnectionPool().getFactory(); + assertTrue(pcf.isFastFailValidation()); + assertTrue(pcf.getDisconnectionIgnoreSqlCodes().contains("XXXX")); + assertEquals(1, pcf.getDisconnectionIgnoreSqlCodes().size()); + } + } + /** * JIRA DBCP-333: Check that a custom class loader is used. * @throws Exception diff --git a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java index 246fef15..d1227b0d 100644 --- a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java +++ b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java @@ -83,6 +83,11 @@ public class TestBasicDataSourceMXBean { return null; } + @Override + public String[] getDisconnectionIgnoreSqlCodesAsArray() { + return null; + } + @Override public String getDriverClassName() { return null; diff --git a/src/test/java/org/apache/commons/dbcp2/TestPoolableConnection.java b/src/test/java/org/apache/commons/dbcp2/TestPoolableConnection.java index 30611d8c..4b7681fe 100644 --- a/src/test/java/org/apache/commons/dbcp2/TestPoolableConnection.java +++ b/src/test/java/org/apache/commons/dbcp2/TestPoolableConnection.java @@ -25,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.fail; import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import javax.management.OperationsException; @@ -182,6 +183,27 @@ public class TestPoolableConnection { TestBasicDataSourceMXBean.testMXBeanCompliance(PoolableConnectionMXBean.class); } + @Test + public void testDisconnectionIgnoreSqlCodes() throws Exception { + pool.setTestOnReturn(true); + final PoolableConnectionFactory factory = (PoolableConnectionFactory) pool.getFactory(); + factory.setFastFailValidation(true); + factory.setDisconnectionIgnoreSqlCodes(Arrays.asList("08S02", "08007")); + + final PoolableConnection conn = pool.borrowObject(); + final TesterConnection nativeConnection = (TesterConnection) conn.getInnermostDelegate(); + + // set up non-fatal exception + nativeConnection.setFailure(new SQLException("Non-fatal connection error.", "08S02")); + assertThrows(SQLException.class, conn::createStatement); + nativeConnection.setFailure(null); + + // verify that non-fatal connection is returned to the pool + conn.close(); + assertEquals(0, pool.getNumActive(), "The pool should have no active connections"); + assertEquals(1, pool.getNumIdle(), "The pool should have one idle connection"); + } + // Bugzilla Bug 33591: PoolableConnection leaks connections if the // delegated connection closes itself. @Test diff --git a/src/test/java/org/apache/commons/dbcp2/TestUtils.java b/src/test/java/org/apache/commons/dbcp2/TestUtils.java index 490ee6d6..30bd8715 100644 --- a/src/test/java/org/apache/commons/dbcp2/TestUtils.java +++ b/src/test/java/org/apache/commons/dbcp2/TestUtils.java @@ -19,12 +19,70 @@ package org.apache.commons.dbcp2; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class TestUtils { public static PStmtKey getPStmtKey(final PoolablePreparedStatement<PStmtKey> poolablePreparedStatement) { return poolablePreparedStatement.getKey(); } + @Test + public void testCheckForConflictsWithOverlap() { + Collection<String> codes1 = new HashSet<>(Arrays.asList("08003", "08006")); + Collection<String> codes2 = new HashSet<>(Arrays.asList("08005", "08006")); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + Utils.checkForConflicts(codes1, codes2, "codes1", "codes2"); + }); + + assertEquals("08006 cannot be in both codes1 and codes2.", exception.getMessage()); + } + + @Test + public void testCheckForConflictsNoOverlap() { + Collection<String> codes1 = new HashSet<>(Arrays.asList("08003", "08006")); + Collection<String> codes2 = new HashSet<>(Arrays.asList("08005", "08007")); + + assertDoesNotThrow(() -> Utils.checkForConflicts(codes1, codes2, "codes1", "codes2")); + } + + @Test + public void testCheckForConflictsFirstCollectionNull() { + Collection<String> codes1 = null; + Collection<String> codes2 = new HashSet<>(Arrays.asList("08005", "08007")); + + assertDoesNotThrow(() -> Utils.checkForConflicts(codes1, codes2, "codes1", "codes2")); + } + + @Test + public void testCheckForConflictsSecondCollectionNull() { + Collection<String> codes1 = new HashSet<>(Arrays.asList("08003", "08006")); + Collection<String> codes2 = null; + + assertDoesNotThrow(() -> Utils.checkForConflicts(codes1, codes2, "codes1", "codes2")); + } + + @Test + public void testCheckForConflictsBothCollectionsNull() { + assertDoesNotThrow(() -> Utils.checkForConflicts(null, null, "codes1", "codes2")); + } + + @Test + public void testCheckForConflictsEmptyCollections() { + Collection<String> codes1 = Collections.emptySet(); + Collection<String> codes2 = Collections.emptySet(); + + assertDoesNotThrow(() -> Utils.checkForConflicts(codes1, codes2, "codes1", "codes2")); + } + @Test public void testClassLoads() { Utils.closeQuietly((AutoCloseable) null);