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 411cae93b3bcb5feff261abbd5a595f0f50ec97c
Author: opencode <[email protected]>
AuthorDate: Mon Nov 2 21:32:11 2026 +0100

    Label pool exhaustion with an SQLState so state-based classifiers can see it
    
    PoolExhaustedException offers the full set of SQLException constructors,
    including every SQLState-aware one, but the pool throws through the
    message-only constructor at its two exhaustion exits, so no production
    caller ever used the others and getSQLState() was always null for pool
    exhaustion. An application multiplexing many SQLException sources and
    classifying them by state - a common pattern for retry layers that
    avoid depending on pool-internal classes - therefore could not
    recognize the most common pool failure at all and had to fall back to
    referencing the pool class or matching message text.
    
    The class now declares the state it stands for as a public constant,
    SQL_STATE = "HY000", and the message-only constructor - the one both
    throw sites use - stamps it. HY000, the general-error class, was chosen
    over the 08xxx connection family because pool exhaustion is a pool-side
    condition rather than the session itself failing; the constant's
    javadoc records that reasoning. Constructors given an explicit state
    keep what they were given.
    
    TestPoolExhaustionSqlState pins the contract at the two meaningful
    levels: the constructor contract (message-only carries the state, an
    explicit state wins), and the application-visible timeout exit through
    DataSource.getConnection(), which is the only exhaustion variant that
    reaches a caller - the no-wait variant is consumed by the asynchronous
    borrow, which converts it into a wait.
---
 .../tomcat/jdbc/pool/PoolExhaustedException.java   | 15 +++-
 .../jdbc/pool/TestPoolExhaustionSqlState.java      | 80 ++++++++++++++++++++++
 2 files changed, 93 insertions(+), 2 deletions(-)

diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java
index ab635a2c09..d47223e468 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolExhaustedException.java
@@ -25,6 +25,16 @@ public class PoolExhaustedException extends SQLException {
 
     private static final long serialVersionUID = 3501536931777262475L;
 
+    /**
+     * SQL state reported with pool-exhaustion failures: {@code "HY000"}, the 
general
+     * error class. Pool exhaustion is a pool-side condition rather than a 
database
+     * error, so none of the connection-class states (the {@code 08xxx} 
family, which
+     * describe the session itself failing) says exactly this; still, 
consumers that
+     * multiplex many {@link SQLException} sources and classify them by state 
need a
+     * non-null label to recognize pool exhaustion without referencing pool 
classes.
+     */
+    public static final String SQL_STATE = "HY000";
+
     /**
      * Constructs a PoolExhaustedException with no detail message.
      */
@@ -32,12 +42,13 @@ public class PoolExhaustedException extends SQLException {
     }
 
     /**
-     * Constructs a PoolExhaustedException with the specified detail message.
+     * Constructs a PoolExhaustedException with the specified detail message. 
The
+     * exception carries {@link #SQL_STATE} as its SQL state.
      *
      * @param reason the detail message
      */
     public PoolExhaustedException(String reason) {
-        super(reason);
+        super(reason, SQL_STATE);
     }
 
     /**
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolExhaustionSqlState.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolExhaustionSqlState.java
new file mode 100644
index 0000000000..52d55efb62
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolExhaustionSqlState.java
@@ -0,0 +1,80 @@
+/*
+ *  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 org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.JdbcPoolBaseTest;
+
+/**
+ * What a failed borrow says about itself to a caller that classifies
+ * {@link java.sql.SQLException}s by state.
+ *
+ * <p>The pool-exhaustion exits throw {@link PoolExhaustedException},
+ * and it used to leave {@code getSQLState()} null: an application
+ * multiplexing many {@code SQLException} sources could only recognize
+ * pool exhaustion by referencing the pool's class or by matching
+ * message text. The exhaustion exception now carries its documented
+ * state, stamped by the constructor the pool throws through, so a
+ * state-based classifier sees the condition.</p>
+ */
+class TestPoolExhaustionSqlState extends JdbcPoolBaseTest {
+
+    /**
+     * The class-level contract: the message-only constructor - the one
+     * both the no-wait refusal and the wait timeout throw through -
+     * stamps the documented state, while a constructor given an
+     * explicit state keeps that state.
+     */
+    @Test
+    void messageConstructorStampsTheStateAndAnExplicitStateWins() {
+        Assertions.assertEquals(PoolExhaustedException.SQL_STATE,
+                new PoolExhaustedException("pool empty").getSQLState(),
+                "the message-only exhaustion must carry the documented "
+                        + "state, not a null one");
+        Assertions.assertEquals("08001",
+                new PoolExhaustedException("pool empty", 
"08001").getSQLState(),
+                "an explicitly given state must be kept as given");
+    }
+
+    /**
+     * The application-visible end of the contract: the exhaustion a
+     * timing-out borrow fails with reaches the caller carrying the
+     * state, not null.
+     */
+    @Test
+    void waitTimeoutReachesTheCallerWithTheExhaustionState() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setInitialSize(0);
+        ds.getPoolProperties().setMaxActive(1);
+        ds.getPoolProperties().setMaxWait(250);
+        ds.createPool();
+        try (Connection held = ds.getConnection()) {
+            PoolExhaustedException exhausted = Assertions.assertThrows(
+                    PoolExhaustedException.class, ds::getConnection,
+                    "a timing-out borrow on an exhausted pool must fail "
+                            + "with the exhaustion type");
+            Assertions.assertEquals(PoolExhaustedException.SQL_STATE,
+                    exhausted.getSQLState(),
+                    "the exhaustion that reaches the caller must carry the "
+                            + "documented state, not a null one");
+        }
+    }
+}


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

Reply via email to