This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 364d4ba58b79d4267a68a0e25f2cb2b286964266
Author: opencode <[email protected]>
AuthorDate: Tue Nov 3 16:15:35 2026 +0100

    Guard the pool against throwables that out-type its catches
    
    The guards around a pool validation - the custom Validator call, the
    driver's isValid() check and the validation statement - converted
    exceptions into a failed check but let Errors through. An Error is not
    far from routine behind a Validator or a driver wrapper loaded through
    a web application class loader: a LinkageError, most often the
    NoClassDefFoundError of a context that has since stopped, is that
    loader's ordinary failure. Such an Error unwound the borrow, return or
    idle path after the pooled connection had left one queue and before it
    reached the other, stranding it outside both - invisible to the
    abandoned check, to the eviction sweep and to the pool's close alike,
    and still counted against maxActive for the lifetime of the JVM.
    
    The scheduled maintenance task was the worse arrival. Its guard caught
    only exceptions, and a throwable escaping a timer task cancels
    java.util.Timer for good: every pool shares one cleaner timer, so one
    pool's Error silently ended eviction and abandoned-check for all of
    them, and the pool-side field still held the dead timer as if it were
    live, so the next pool that needed a cleaner failed to start, with
    "IllegalStateException: Timer already cancelled" for the rest of the
    JVM's life.
    
    The validation guards now apply the module's fatal policy through
    ExceptionUtils.handleThrowable and report everything short of it as
    the failed check that the throw says the session is. The maintenance
    task runs its body's arrivals through the same policy, so nothing
    except a fatal error can interrupt it, and reports the rest under the
    new connectionPool.poolCleanerFailed message. A shared timer that
    arrives dead is replaced rather than obeyed, so even a timer lost to a
    fatal error costs nothing after the registration that followed it.
    
    The pins: the LinkageError validator now fails borrows, returns and
    sweeps exactly like the throwing one - a checked exception at the
    application, no stranding, the slot whole - while a scheduled sweep
    broken by an Error leaves the same live shared timer that a new pool
    joins; a dead timer is replaced on the next registration; and a fatal
    error still propagates through a borrow and through the task
    unaltered, so the containment cannot silently grow into swallowing
    what no recovery is written for.
---
 .../apache/tomcat/jdbc/pool/ConnectionPool.java    |  52 ++++--
 .../apache/tomcat/jdbc/pool/PooledConnection.java  |  37 +++-
 .../tomcat/jdbc/pool/util/LocalStrings.properties  |   1 +
 .../tomcat/jdbc/pool/TestSharedCleanerTimer.java   | 187 +++++++++++++++++++++
 .../tomcat/jdbc/pool/TestThrowingValidator.java    |  95 ++++++++++-
 5 files changed, 353 insertions(+), 19 deletions(-)

diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
index 41dcd9cd51..19ac544293 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
@@ -51,6 +51,7 @@ import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.jdbc.pool.interceptor.QueryTimeoutInterceptor;
 import org.apache.tomcat.jdbc.pool.interceptor.StatementCache;
 import org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;
+import org.apache.tomcat.jdbc.pool.util.ExceptionUtils;
 import org.apache.tomcat.jdbc.pool.util.MessageFactory;
 
 /**
@@ -2779,22 +2780,35 @@ public class ConnectionPool {
     private static synchronized void registerCleaner(PoolCleaner cleaner) {
         unregisterCleaner(cleaner);
         cleaners.add(cleaner);
-        if (poolCleanTimer == null) {
-            ClassLoader loader = 
Thread.currentThread().getContextClassLoader();
+        // A timer cancels itself when a task throws an uncaught throwable,
+        // and scheduling onto such a dead timer fails - which would make
+        // every pool that needs a cleaner afterwards fail the same way for
+        // the rest of the JVM's life. PoolCleaner.run no longer lets
+        // anything escape; this is the safety net for a timer that is
+        // already dead when it arrives: replace it rather than refuse.
+        while (true) {
+            if (poolCleanTimer == null) {
+                ClassLoader loader = 
Thread.currentThread().getContextClassLoader();
+                try {
+                    
Thread.currentThread().setContextClassLoader(ConnectionPool.class.getClassLoader());
+                    // Create the timer thread in a PrivilegedAction so that a
+                    // reference to the web application class loader is not 
created
+                    // via Thread.inheritedAccessControlContext
+                    poolCleanTimer = new Timer(
+                            "Tomcat JDBC Pool Cleaner[" + 
System.identityHashCode(ConnectionPool.class.getClassLoader()) +
+                                    ":" + System.currentTimeMillis() + "]",
+                            true);
+                } finally {
+                    Thread.currentThread().setContextClassLoader(loader);
+                }
+            }
             try {
-                
Thread.currentThread().setContextClassLoader(ConnectionPool.class.getClassLoader());
-                // Create the timer thread in a PrivilegedAction so that a
-                // reference to the web application class loader is not created
-                // via Thread.inheritedAccessControlContext
-                poolCleanTimer = new Timer(
-                        "Tomcat JDBC Pool Cleaner[" + 
System.identityHashCode(ConnectionPool.class.getClassLoader()) +
-                                ":" + System.currentTimeMillis() + "]",
-                        true);
-            } finally {
-                Thread.currentThread().setContextClassLoader(loader);
+                poolCleanTimer.schedule(cleaner, cleaner.sleepTime, 
cleaner.sleepTime);
+                return;
+            } catch (IllegalStateException x) {
+                poolCleanTimer = null;
             }
         }
-        poolCleanTimer.schedule(cleaner, cleaner.sleepTime, cleaner.sleepTime);
     }
 
     private static synchronized void unregisterCleaner(PoolCleaner cleaner) {
@@ -2896,8 +2910,16 @@ public class ConnectionPool {
                     } else if (pool.getPoolProperties().getMaxAge() > 0) {
                         pool.testAllIdle(true);
                     }
-                } catch (Exception x) {
-                    log.error(x.toString(), x);
+                } catch (Throwable x) {
+                    // The guards on the maintenance paths convert exceptions
+                    // and - since handleThrowable joined them - non-fatal
+                    // Errors, but a throwable can still reach here from a
+                    // path without a guard of its own. The module's fatal
+                    // policy runs first, as everywhere else; losing this
+                    // task's next runs to a timer that dies on a fatal Error
+                    // is borne by the replacement in registerCleaner.
+                    ExceptionUtils.handleThrowable(x);
+                    
log.error(MessageFactory.get("connectionPool.poolCleanerFailed"), x);
                 }
             }
         }
diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java
index 46ea99ddb2..d6746f9cb2 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java
@@ -33,6 +33,7 @@ import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;
 import org.apache.tomcat.jdbc.pool.jmx.JmxUtil;
+import org.apache.tomcat.jdbc.pool.util.ExceptionUtils;
 import org.apache.tomcat.jdbc.pool.util.MessageFactory;
 
 /**
@@ -943,13 +944,20 @@ public class PooledConnection implements 
PooledConnectionMBean {
             boolean valid;
             try {
                 valid = poolProperties.getValidator().validate(connection, 
validateAction);
-            } catch (RuntimeException x) {
+            } catch (Throwable x) {
+                ExceptionUtils.handleThrowable(x);
                 // The SPI declares no checked exception, so throwing is the
                 // only way an implementation can report its own failure; the
                 // pool must treat that as a failed validation, because a
                 // throw escaping from here unwinds the borrow or return path
                 // between the idle and busy queues and strands the pooled
-                // connection - permanently shrinking the pool
+                // connection - permanently shrinking the pool. Errors belong
+                // in this guard too: an implementation loaded through a web
+                // application class loader fails with LinkageError variants -
+                // a NoClassDefFoundError once the context has stopped is the
+                // common one - and that is a check that cannot vouch for the
+                // session, nothing else. Only the fatal conditions that
+                // handleThrowable re-throws keep propagating.
                 if (getPoolProperties().getLogValidationErrors()) {
                     
log.error(MessageFactory.get("pooledConnection.customValidationThrew",
                             poolProperties.getValidator()), x);
@@ -1034,6 +1042,23 @@ public class PooledConnection implements 
PooledConnectionMBean {
                     log.debug("isValid() failed.", e);
                 }
                 return false;
+            } catch (Throwable x) {
+                // Same reasoning one step further: a driver or wrapper that
+                // fails the check with an Error - a LinkageError surfacing
+                // through a wrapper whose class loader has lost a class is
+                // the realistic arrival - has answered the question the
+                // check asks, "can this session be vouched for", with
+                // nothing better than an exception would; handleThrowable
+                // keeps the fatal conditions it keeps everywhere else.
+                ExceptionUtils.handleThrowable(x);
+                if (getPoolProperties().getLogValidationErrors()) {
+                    
log.error(MessageFactory.get("pooledConnection.isValidFailed"), x);
+                } else {
+                    // Logged like the unchecked case above: this is the
+                    // same anomaly one type further out
+                    
log.warn(MessageFactory.get("pooledConnection.isValidFailed"), x);
+                }
+                return false;
             } finally {
                 if (!transactionTerminated) {
                     silentlyRollbackTransactionIfNeeded();
@@ -1075,7 +1100,13 @@ public class PooledConnection implements 
PooledConnectionMBean {
                 transactionTerminated = silentlyRollbackTransactionIfNeeded();
             }
             return transactionTerminated;
-        } catch (Exception ex) {
+        } catch (Throwable ex) {
+            // The checked and unchecked failures of the driver calls above
+            // are failed validations, and so is an Error short of the fatal
+            // conditions handleThrowable keeps: an escaping throw unwinds
+            // the borrow path between the idle and busy queues and strands
+            // the pooled connection, whatever the throw's type.
+            ExceptionUtils.handleThrowable(ex);
             if (getPoolProperties().getLogValidationErrors()) {
                 
log.error(MessageFactory.get("pooledConnection.sqlValidationError"), ex);
             } else if (log.isDebugEnabled()) {
diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties
index 29c076edea..047bcb2315 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties
@@ -53,6 +53,7 @@ connectionPool.maxActiveTooSmall=maxActive is smaller than 1, 
setting maxActive
 connectionPool.maxIdleTooLarge=maxIdle is larger than maxActive, setting 
maxIdle to: {0}
 connectionPool.maxIdleTooSmall=maxIdle is smaller than minIdle, setting 
maxIdle to: {0}
 connectionPool.minIdleTooLarge=minIdle is larger than maxActive, setting 
minIdle to: {0}
+connectionPool.poolCleanerFailed=The pool maintenance task failed, it will run 
again at the next scheduled interval.
 connectionPool.poolClosed=Connection pool closed.
 connectionPool.poolEmptyNoWait=[{0}] NoWait: Pool empty. Unable to fetch a 
connection, none available[{1} in use].
 connectionPool.poolEmptyTimeout=[{0}] Timeout: Pool empty. Unable to fetch a 
connection in {1} seconds, none available[size:{2}; busy:{3}; idle:{4}; 
lastwait:{5}].
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestSharedCleanerTimer.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestSharedCleanerTimer.java
new file mode 100644
index 0000000000..a70990cb42
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestSharedCleanerTimer.java
@@ -0,0 +1,187 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.jdbc.pool;
+
+import java.sql.Connection;
+import java.util.Timer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.JdbcPoolBaseTest;
+
+/**
+ * One {@link java.util.Timer} serves the maintenance of every pool the
+ * class loader holds, and a timer cancels itself for good the moment a
+ * task lets a throwable escape - so an escaping {@code Error} from the
+ * cleaner's body is not one pool's accident but the end of eviction and
+ * abandoned-check for every pool in the JVM, later pool starts included:
+ * scheduling onto the dead timer fails with {@code IllegalStateException}
+ * and the pool-side field still holds it as if it were live. These tests
+ * pin the three parts of that contract: an ordinary {@code Error} inside
+ * the swept work is contained and the shared timer goes on serving, a
+ * timer that arrives dead is replaced rather than obeyed, and - so the
+ * containment cannot silently grow into swallowing the unrecoverable -
+ * the fatal conditions ExceptionUtils keeps fatal still propagate, from
+ * a borrow and from the task alike.
+ */
+class TestSharedCleanerTimer extends JdbcPoolBaseTest {
+
+    /**
+     * Fails the idle sweep's check the way a stopped web-application class
+     * loader fails an interceptor: a LinkageError the maintenance work can
+     * neither predict nor need. Every other action answers normally, so
+     * the sweep is the only place the error can arrive.
+     */
+    public static class SweepErrorValidator implements Validator {
+        private final AtomicInteger idleCalls = new AtomicInteger();
+
+        @Override
+        public boolean validate(Connection connection, int validateAction) {
+            if (validateAction == PooledConnection.VALIDATE_IDLE) {
+                idleCalls.incrementAndGet();
+                throw new NoClassDefFoundError("the context this validator was 
loaded from is stopped");
+            }
+            return true;
+        }
+    }
+
+    /** Reports every check fatal, whatever asked for it. */
+    public static class FatalValidator implements Validator {
+        private final AtomicInteger calls = new AtomicInteger();
+
+        @Override
+        public boolean validate(Connection connection, int validateAction) {
+            calls.incrementAndGet();
+            throw new OutOfMemoryError("the validator reports a dying JVM");
+        }
+    }
+
+    @Test
+    void anErrorInsideTheSweptWorkLeavesTheSharedTimerServing() throws 
Exception {
+        SweepErrorValidator validator = new SweepErrorValidator();
+        DataSource first = newDataSource();
+        first.getPoolProperties().setInitialSize(0);
+        // the task's checkIdle step evicts idle connections down to min
+        // idle; park the floor at the size this test keeps so the sweep is
+        // the only thing the task can do to the connection
+        first.getPoolProperties().setMinIdle(1);
+        first.getPoolProperties().setTestWhileIdle(true);
+        first.getPoolProperties().setTimeBetweenEvictionRunsMillis(200);
+        first.getPoolProperties().setValidationInterval(0);
+        first.getPoolProperties().setValidator(validator);
+        first.createPool();
+        Timer timer = ConnectionPool.getPoolTimer();
+        Assertions.assertNotNull(timer);
+
+        Connection firstBorrowed = first.getConnection();
+        firstBorrowed.close();
+        // let the scheduled sweep really run into the error
+        awaitUntil(() -> validator.idleCalls.get() >= 1);
+
+        // the swept connection was read as a failed check and taken out of
+        // service - not stranded mid-sweep
+        Assertions.assertEquals(0, first.getPool().getIdle());
+        Assertions.assertEquals(0, first.getPool().getSize());
+
+        // the timer outlived the sweep: a second pool starts on the very
+        // same timer a dying task would have cancelled
+        DataSource next = newDataSource();
+        next.getPoolProperties().setTimeBetweenEvictionRunsMillis(200);
+        next.createPool();
+        Assertions.assertSame(timer, ConnectionPool.getPoolTimer(),
+                "the shared timer must still be the live one the second pool 
joined");
+        try (Connection c = next.getConnection()) {
+            Assertions.assertNotNull(c);
+        }
+
+        next.close();
+        first.close();
+    }
+
+    @Test
+    void afatalErrorInsideTheSweptWorkStillPropagatesFromTheTask() throws 
Exception {
+        FatalValidator validator = new FatalValidator();
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setInitialSize(0);
+        ds.getPoolProperties().setMinIdle(1);
+        ds.getPoolProperties().setTestWhileIdle(true);
+        // the sweep runs here, on this thread, exactly as the shared timer
+        // would run it - and no background competitor for the idle queue
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(0);
+        ds.getPoolProperties().setValidationInterval(0);
+        ds.getPoolProperties().setValidator(validator);
+        ds.createPool();
+        Connection first = ds.getConnection();
+        first.close();
+        Assertions.assertEquals(1, ds.getPool().getIdle());
+
+        ConnectionPool.PoolCleaner cleaner = new 
ConnectionPool.PoolCleaner(ds.getPool(), 100);
+
+        // the task-wide guard reports and keeps the module's fatal policy:
+        // a VirtualMachineError is beyond any per-call recovery and keeps
+        // propagating from the pool, the task and the borrow alike
+        Assertions.assertThrows(OutOfMemoryError.class, cleaner::run);
+        Assertions.assertTrue(validator.calls.get() >= 1,
+                "the sweep must have run into the error the guard is tested 
against");
+    }
+
+    @Test
+    void aDeadSharedTimerIsReplacedWhenANewPoolNeedsOne() throws Exception {
+        DataSource first = newDataSource();
+        first.getPoolProperties().setTimeBetweenEvictionRunsMillis(500);
+        first.createPool();
+        Timer dead = ConnectionPool.getPoolTimer();
+        Assertions.assertNotNull(dead, "a pool with a sweeper must hold the 
shared timer");
+
+        // The state a task that died on a fatal error leaves behind: the
+        // timer cancelled for good while the field still points at it as
+        // if live.
+        dead.cancel();
+
+        DataSource next = newDataSource();
+        next.getPoolProperties().setTimeBetweenEvictionRunsMillis(500);
+        Assertions.assertDoesNotThrow(next::createPool,
+                "a dead shared timer must not make every later pool fail to 
start");
+        Assertions.assertNotSame(dead, ConnectionPool.getPoolTimer(),
+                "the refusal must have been read as a dead timer and answered 
with a fresh one");
+        try (Connection c = next.getConnection()) {
+            Assertions.assertNotNull(c);
+        }
+
+        next.close();
+        first.close();
+    }
+
+    @Test
+    void afatalErrorStillPropagatesThroughABorrow() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setInitialSize(0);
+        ds.getPoolProperties().setMaxActive(1);
+        ds.getPoolProperties().setTestOnBorrow(true);
+        ds.getPoolProperties().setValidationInterval(0);
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(0);
+        ds.getPoolProperties().setValidator(new FatalValidator());
+        // the creating borrow is not validated; the re-borrow is
+        Connection first = ds.getConnection();
+        first.close();
+
+        Assertions.assertThrows(OutOfMemoryError.class, ds::getConnection);
+        ds.close();
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java
index db922762bc..91edd4b710 100644
--- 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingValidator.java
@@ -31,6 +31,12 @@ import org.apache.tomcat.jdbc.JdbcPoolBaseTest;
  * escape {@code getConnection()}/{@code close()}, must not strand the
  * pooled connection between the idle and busy queues (a permanently lost
  * slot), and must not silently disable idle eviction.
+ *
+ * <p>An {@code Error} thrown from behind an implementation counts the
+ * same way: an implementation loaded through a web-application class
+ * loader fails with {@code LinkageError} variants long after any
+ * deliberate throw, and the guard must be as wide as the stranding it
+ * prevents - fatal JVM errors excepted.</p>
  */
 class TestThrowingValidator extends JdbcPoolBaseTest {
 
@@ -42,6 +48,18 @@ class TestThrowingValidator extends JdbcPoolBaseTest {
         }
     }
 
+    /**
+     * The broken implementation of the stopped-web-application case: a
+     * class the implementation can no longer reach fails every call with
+     * a LinkageError, which no implementation author throws on purpose.
+     */
+    public static class ErrorThrowingValidator implements Validator {
+        @Override
+        public boolean validate(Connection connection, int validateAction) {
+            throw new NoClassDefFoundError("the context this validator was 
loaded from is stopped");
+        }
+    }
+
     /** A validator that only fails, the way the throwing one now fails. */
     public static class RejectingValidator implements Validator {
         @Override
@@ -52,6 +70,11 @@ class TestThrowingValidator extends JdbcPoolBaseTest {
 
     /* One-slot pool with a throwing validator, gated on by the caller's test 
flag. */
     private DataSource throwingPool() {
+        return throwingPool(new ThrowingValidator());
+    }
+
+    /* As above, with the implementation of the caller's choosing. */
+    private DataSource throwingPool(Validator validator) {
         DataSource ds = newDataSource();
         ds.getPoolProperties().setInitialSize(0);
         // exactly one slot: any leaked slot exhausts the pool immediately
@@ -61,7 +84,7 @@ class TestThrowingValidator extends JdbcPoolBaseTest {
         // background sweep would now be a working competitor for the idle
         // assertions below
         ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(0);
-        ds.getPoolProperties().setValidator(new ThrowingValidator());
+        ds.getPoolProperties().setValidator(validator);
         return ds;
     }
 
@@ -147,6 +170,76 @@ class TestThrowingValidator extends JdbcPoolBaseTest {
         Assertions.assertEquals(0, ds.getPool().getSize());
     }
 
+    @Test
+    void errorThrowingValidatorOnBorrowFailsAsCheckedException() throws 
Exception {
+        DataSource ds = throwingPool(new ErrorThrowingValidator());
+        ds.getPoolProperties().setTestOnBorrow(true);
+        // The stopped-class-loader failure mode: the LinkageError must be
+        // read as a check that cannot vouch for the session, not as a
+        // condition beyond the pool's handling - a throw escaping the
+        // borrow would strand the session outside both queues, where no
+        // drain, sweep or abandoned check can ever reach it.
+        Connection first = ds.getConnection();
+        awaitSessions(1);
+        first.close();
+        Assertions.assertEquals(1, ds.getPool().getIdle());
+
+        SQLException sqlX = Assertions.assertThrows(SQLException.class, 
ds::getConnection);
+        Assertions.assertTrue(sqlX.getMessage().contains("Failed to validate"),
+                "expected a validation failure, got: " + sqlX.getMessage());
+        awaitSessions(0);
+        Assertions.assertEquals(0, ds.getPool().getSize());
+
+        // and the slot survived its own validator: the pool is whole
+        ds.getPoolProperties().setValidator(null);
+        try (Connection again = ds.getConnection()) {
+            Assertions.assertNotNull(again);
+        }
+        // the returned session parks in idle, where it is whole and
+        // reusable - the failed borrow cost the pool nothing
+        Assertions.assertEquals(1, ds.getPool().getIdle());
+        Assertions.assertEquals(1, ds.getPool().getSize());
+        ds.close();
+        awaitSessions(0);
+    }
+
+    @Test
+    void errorThrowingValidatorOnReturnDoesNotEscapeOrStrand() throws 
Exception {
+        DataSource ds = throwingPool(new ErrorThrowingValidator());
+        ds.getPoolProperties().setTestOnReturn(true);
+        Connection first = ds.getConnection();
+        awaitSessions(1);
+
+        Assertions.assertDoesNotThrow(first::close);
+        awaitSessions(0);
+        Assertions.assertEquals(0, ds.getPool().getIdle());
+        Assertions.assertEquals(0, ds.getPool().getActive());
+        Assertions.assertEquals(0, ds.getPool().getSize());
+
+        ds.getPoolProperties().setValidator(null);
+        try (Connection again = ds.getConnection()) {
+            Assertions.assertNotNull(again);
+        }
+        ds.close();
+        awaitSessions(0);
+    }
+
+    @Test
+    void errorThrowingValidatorOnIdleValidationDoesNotAbortTheSweep() throws 
Exception {
+        DataSource ds = throwingPool(new ErrorThrowingValidator());
+        ds.getPoolProperties().setTestWhileIdle(true);
+        Connection first = ds.getConnection();
+        first.close();
+        awaitSessions(1);
+        Assertions.assertEquals(1, ds.getPool().getIdle());
+
+        // the Error must not escape the sweep with the tested connection
+        // claimed out of idle and stranded, nor leave it parked untested
+        Assertions.assertDoesNotThrow(ds::testIdle);
+        awaitSessions(0);
+        Assertions.assertEquals(0, ds.getPool().getIdle());
+    }
+
     @Test
     void rejectingValidatorOnBorrowFailsAndReleasesTheSlot() throws Exception {
         DataSource ds = throwingPool();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to