This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 045b37b68affe2f1887bddfaa9edcca0d2498f03 Author: opencode <[email protected]> AuthorDate: Thu Oct 29 13:43:19 2026 +0100 Count every connection the pool hands to a borrower as a borrow The JDBC pool's BorrowedCount statistic only counted a borrow that took an idle connection off the queue, so a borrow the pool served by creating a connection - the first borrows of any pool that grows on demand - was never counted, and neither was an asynchronous borrow whose connection the fair queue handed over to the waiting poll. The JMX BorrowedCount attribute and getBorrowedCount() therefore under-reported by one for every such borrow, while a connection found released while be [...] --- .../apache/tomcat/jdbc/pool/ConnectionPool.java | 19 ++- .../jdbc/pool/TestBorrowReturnStatistics.java | 130 +++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) 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 6913471d90..a01e024272 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 @@ -331,6 +331,20 @@ public class ConnectionPool { * @throws SQLException if an interceptor can't be configured, if the proxy can't be instantiated */ protected Connection setupConnection(PooledConnection con) throws SQLException { + // The connection is being handed to a borrower: a borrow is counted + // here rather than where it was claimed, as that is the one point all + // hand-offs pass - the synchronous borrows, the async borrow that + // takes an idle connection, the configuration of a connection a queue + // handed to an async poll, and the cleanup that retrieves a cancelled + // poll's connection to return it - each exactly once, while a claim + // that ends in a throw never reaches a borrower at all. A setup that + // fails gives the connection back through abandonSetup(), which counts + // the return, so the borrow is counted on the way in to match. The + // pool's own initialization is the only path that takes connections + // out of the creation route without a borrower: it returns its + // preloaded connections directly and never sets one up here, so it + // stays out of the count. + borrowedCount.incrementAndGet(); // Fetch previously cached interceptor proxy - one per connection JdbcInterceptor handler = con.getHandler(); if (handler == null) { @@ -873,10 +887,13 @@ public class ConnectionPool { if (con != null) { // Configure the connection and return it PooledConnection result = borrowConnection(now, con, username, password); - borrowedCount.incrementAndGet(); if (result != null) { return result; } + // The connection was released while it sat idle, so this + // poll got nothing to hand out; keep polling. Nothing is + // counted here: a borrow is counted on the hand-off that + // succeeds, in setupConnection(). } // If we get here, see if we need to create one diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestBorrowReturnStatistics.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestBorrowReturnStatistics.java new file mode 100644 index 0000000000..0a76c60271 --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestBorrowReturnStatistics.java @@ -0,0 +1,130 @@ +/* + * 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.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.apache.tomcat.jdbc.JdbcPoolBaseTest; + +/** + * Expected functionality of the pool-level borrow and return counters + * that monitoring reads as the {@code BorrowedCount} and + * {@code ReturnedCount} attributes: every successful hand-off of a + * connection to a borrower counts as one borrow, no matter which path + * served it - a newly created connection, an idle one, or a connection a + * queue hands to an asynchronous poll - while the physical creations + * counted separately only follow the connections the pool actually opens. + */ +class TestBorrowReturnStatistics extends JdbcPoolBaseTest { + + @Test + void aBorrowServedByANewConnectionCountsAsABorrow() throws Exception { + DataSource ds = newDataSource(); + ds.getPoolProperties().setInitialSize(0); + ds.getPoolProperties().setMinIdle(0); + ds.getPoolProperties().setMaxActive(1); + ds.createPool(); + ConnectionPool pool = ds.getPool(); + Assertions.assertEquals(0, pool.getBorrowedCount()); + Assertions.assertEquals(0, pool.getReturnedCount()); + Assertions.assertEquals(0, pool.getCreatedCount()); + + // served by creating a connection: the pool has none idle to give + Connection created = ds.getConnection(); + Assertions.assertEquals(1, pool.getBorrowedCount(), + "a borrower that received a freshly created connection " + + "must count as a borrow, the same way one that " + + "received an idle connection does"); + Assertions.assertEquals(1, pool.getCreatedCount(), + "the physical creation itself must still count as one creation"); + created.close(); + Assertions.assertEquals(1, pool.getReturnedCount()); + + // served from idle: the counters must step exactly the same way + Connection reused = ds.getConnection(); + Assertions.assertEquals(2, pool.getBorrowedCount()); + Assertions.assertEquals(1, pool.getCreatedCount(), + "reusing the idle connection must not create anything"); + reused.close(); + Assertions.assertEquals(2, pool.getReturnedCount(), + "once every connection has been returned, the returned " + + "count must equal the borrowed count"); + awaitSessions(1); + } + + @Test + void preCreatedConnectionsAreNotBorrows() throws Exception { + DataSource ds = newDataSource(); + ds.getPoolProperties().setInitialSize(2); + ds.createPool(); + ConnectionPool pool = ds.getPool(); + Assertions.assertEquals(2, pool.getCreatedCount(), + "the initial connections are physically created"); + Assertions.assertEquals(0, pool.getBorrowedCount(), + "the pool pre-creating its initialSize connections hands " + + "them to no borrower, so none of them is a borrow"); + } + + @Test + void anAsyncBorrowCountsAsABorrowOnEitherPath() throws Exception { + DataSource ds = newDataSource(); + ds.getPoolProperties().setFairQueue(true); + ds.getPoolProperties().setInitialSize(0); + ds.getPoolProperties().setMinIdle(0); + ds.getPoolProperties().setMaxActive(1); + ds.createPool(); + ConnectionPool pool = ds.getPool(); + + // put one idle connection up for the pool to hand out (a synchronous + // borrow served by creation) + ds.getConnection().close(); + Assertions.assertEquals(1, pool.getBorrowedCount()); + + // a future for an already available connection: this borrow is served + // right away, the retrieval from the queue never runs + Future<Connection> ready = ds.getConnectionAsync(); + Assertions.assertEquals(2, pool.getBorrowedCount(), + "an async borrow that takes an idle connection is one borrow"); + ready.get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS).close(); + Assertions.assertEquals(2, pool.getReturnedCount()); + + // a queued future: the only connection is held, so the poll waits on + // the queue and the connection is handed to it as it comes back + Connection holder = ds.getConnection(); + Future<Connection> queued = ds.getConnectionAsync(); + Assertions.assertFalse(queued.isDone(), + "the poll must wait for a connection the pool cannot spare"); + holder.close(); + try (Connection served = queued.get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + Assertions.assertEquals(4, pool.getBorrowedCount(), + "the connection the queue handed to the waiting poll is " + + "one borrow, counted once it is set up for " + + "the borrower that retrieves it"); + } + Assertions.assertEquals(4, pool.getReturnedCount(), + "once every connection has been returned, the returned " + + "count must equal the borrowed count"); + Assertions.assertEquals(1, pool.getCreatedCount(), + "every borrow here rode the same single physical connection"); + awaitSessions(1); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
