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);

Reply via email to