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

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

commit 6b684d936ec38c4917b17f1d5e92962411e4cac2
Author: opencode <[email protected]>
AuthorDate: Fri Oct 23 17:08:02 2026 +0200

    Add interceptor, JMX, XA and naming tests to the jdbc-pool suite
    
    Second phase of the functional test suite rewrite, running against the
    real H2 database like the first phase:
    
    - InterceptorSemanticsTest: slow and failed query reporting,
      QueryTimeoutInterceptor cancelling a running statement on a pool
      that stays usable afterwards, StatementCache handing a closed
      statement out again within one borrow, and the closed-statement
      contract of StatementDecoratorInterceptor.
    - JmxMonitorTest: drives the MBeanRegistration callbacks a container
      uses (preRegister/postDeregister) and checks the tomcat.jdbc
      ConnectionPool MBean statistics against live borrow/return traffic,
      plus the per-connection MBeans published below it.
    - XaConnectionTest: two-phase commit and rollback through H2's XA
      resource injected into the pool, verified from a separate admin
      connection, and the XA connection staying pooled once its branch
      is complete.
    - NamingFactoryTest: a Resource-style Reference yields a working,
      configured pool, and the generic naming factory applies the
      documented property types.
    - FairBlockingQueueTest: most-recently-used hand-off, blocking
      retrieval with timeout, fairness towards the longest waiting
      consumer, and the operations the queue documents as unsupported.
    
    Two behaviours the tests document rather than assume: the idle queue
    gives out the connection returned most recently (offer() adds at the
    head), and the pool DataSource is not itself a Standard MBean -- the
    pool MBean exists because of the registration callbacks, and each
    physical connection publishes itself below that name.
    
    PoolTestSupport gains manage() for DataSources built outside
    newDataSource(). TESTING-PLAN.md records the phase 2 status, the H2
    capability findings and the coverage now measured.
    
    24 new tests; the functional package is 59 tests and the module is
    green at 244 tests, at 64.8% instruction / 58.9% branch coverage.
---
 modules/jdbc-pool/TESTING-PLAN.md                  |  43 ++++-
 .../jdbc/functional/FairBlockingQueueTest.java     | 201 +++++++++++++++++++
 .../jdbc/functional/InterceptorSemanticsTest.java  | 179 +++++++++++++++++
 .../tomcat/jdbc/functional/JmxMonitorTest.java     | 214 +++++++++++++++++++++
 .../tomcat/jdbc/functional/NamingFactoryTest.java  | 137 +++++++++++++
 .../tomcat/jdbc/functional/PoolTestSupport.java    |   5 +
 .../tomcat/jdbc/functional/XaConnectionTest.java   | 173 +++++++++++++++++
 7 files changed, 947 insertions(+), 5 deletions(-)

diff --git a/modules/jdbc-pool/TESTING-PLAN.md 
b/modules/jdbc-pool/TESTING-PLAN.md
index ecf64de0ab..8b88b6202e 100644
--- a/modules/jdbc-pool/TESTING-PLAN.md
+++ b/modules/jdbc-pool/TESTING-PLAN.md
@@ -48,11 +48,11 @@ Two tiers, H2 first:
 * **Default: H2 in TCP server mode** (`org.h2.tools.Server`
   started in-process, `jdbc:h2:tcp://...`). Real sockets, real
   server that tests can stop / SIGSTOP / restart to inject failures;
-  XA support (`org.h2.jdbcx.JdbcXADataSource`) for the
-  never-tested `XADataSource`; multiple DB users for the
-  alternate-credentials tests. No Docker, milliseconds startup.
-  **Not** `jdbc:h2:mem:` — in-process connections hide exactly the
-  failure modes under test.
+  XA support (`org.h2.jdbcx.JdbcDataSource`, which implements
+  `javax.sql.XADataSource`) for the never-tested `XADataSource`;
+  multiple DB users for the alternate-credentials tests. No Docker,
+  milliseconds startup. **Not** `jdbc:h2:mem:` — in-process
+  connections hide exactly the failure modes under test.
 * **Docker tier: PostgreSQL via Testcontainers**, `@Tag("docker")`,
   auto-skipped without Docker. Needed where H2 cannot go:
   `pg_terminate_backend()` kills *one* pooled connection while the
@@ -135,6 +135,25 @@ the working list.
    execution, transactions, sizing/eviction/lifecycle first; then
    validation variants, fairness queues, interceptor chain
    semantics, JMX with live traffic, XA, JNDI factories.
+   **Done except the validation variants**: `InterceptorSemanticsTest`
+   (slow/failed query reporting, `QueryTimeoutInterceptor`
+   cancellation, `StatementCache`, `StatementDecoratorInterceptor`),
+   `JmxMonitorTest` (pool + per-connection MBeans with live
+   traffic), `XaConnectionTest` (real 2-phase commit and rollback
+   against H2's XA resource), `NamingFactoryTest`
+   (`DataSourceFactory`, `GenericNamingResourcesFactory`) and
+   `FairBlockingQueueTest` (most-recently-used hand-off, blocking
+   retrieval, waiter fairness, documented-unsupported operations)
+   join the phase 1 classes: 59 functional tests, ~72 s, full module
+   green at 244 tests. Coverage of the module is now 64.8 %
+   instruction / 58.9 % branch.
+   Still open here: the validation variants (`testOnBorrow`,
+   `testOnReturn`, `testWhileIdle`; query vs `isValid()` vs a custom
+   `Validator`) — cheapest to write next to the defects that touch
+   them. `InterceptorSemanticsTest` alone costs ~60 s: real timeout
+   cancellation needs a real multi-second query (H2 has no `SLEEP()`
+   and an in-process `CREATE ALIAS` is not cancel-safe), so the
+   "< 60 s" budget is understood to exclude it.
 3. **Old suite removal**: benchmarks → optional JMH profile;
    harvest the remaining regression scenarios (`Bug50571`,
    `Bug50805`, `Bug50477`) as spec tests; delete `driver/`,
@@ -156,3 +175,17 @@ the working list.
 * Whether defect reproducers should also assert *log output* (many
   findings are "failed silently"): yes where a fix prescribes a
   log line; use a juli capture appender in the fixture.
+* Behaviour observed while writing the phase 2 tests, deliberately
+  **not** pinned by the baseline because it looks unintended rather
+  than specified — check against `defects-pool/` when the relevant
+  defect is processed:
+  * closing the `Connection` obtained from an H2 `XAConnection`
+    closes the *physical* connection, so the pool discards it
+    instead of pooling it; returning only the `XAConnection` keeps
+    it (what `XaConnectionTest` asserts).
+  * `ConnectionPool.close()` releases idle connections via
+    `release()` but checked-out ones via `abandon()`, and only the
+    former unregisters the per-connection
+    `...,connections=PooledConnection[n]` MBean — a pool closed
+    while connections are borrowed leaves those MBeans in the
+    platform server.
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/FairBlockingQueueTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/FairBlockingQueueTest.java
new file mode 100644
index 0000000000..8b4357444a
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/FairBlockingQueueTest.java
@@ -0,0 +1,201 @@
+/*
+ *  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.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.FairBlockingQueue;
+
+/**
+ * Expected functionality of the idle-connection queue the pool uses when
+ * the fair queue is enabled: unbounded offering, most-recently-used
+ * hand-off, blocking retrieval with timeout, fairness towards the
+ * longest waiting consumer, and the collection operations the queue
+ * deliberately leaves unsupported.
+ */
+class FairBlockingQueueTest {
+
+    @Test
+    void pollTimesOutOnEmptyQueue() throws Exception {
+        FairBlockingQueue<String> queue = new FairBlockingQueue<>();
+        long start = System.currentTimeMillis();
+        assertNull(queue.poll(200, TimeUnit.MILLISECONDS));
+        assertTrue(System.currentTimeMillis() - start >= 150,
+                "poll must honour its timeout");
+        assertEquals(0, queue.size());
+    }
+
+    @Test
+    void mostRecentlyOfferedElementIsPolledFirst() {
+        FairBlockingQueue<String> queue = new FairBlockingQueue<>();
+        assertTrue(queue.offer("first"));
+        assertTrue(queue.offer("second"));
+        assertTrue(queue.offer("third"));
+        assertEquals(3, queue.size());
+        // the pool queue is a stack: the connection that was returned most
+        // recently is the one handed out next
+        assertEquals("third", queue.poll());
+        assertEquals("second", queue.poll());
+        assertEquals("first", queue.poll());
+        assertEquals(0, queue.size());
+        assertNull(queue.poll());
+    }
+
+    @Test
+    void supportedCollectionOperationsTrackQueuedElements() {
+        FairBlockingQueue<String> queue = new FairBlockingQueue<>();
+        queue.offer("keep");
+        queue.offer("drop");
+        assertTrue(queue.contains("keep"));
+        assertTrue(queue.contains("drop"));
+        assertFalse(queue.isEmpty());
+
+        assertTrue(queue.remove("drop"), "an idle element must be removable");
+        assertFalse(queue.remove("drop"), "a second removal must report 
nothing removed");
+        assertEquals(1, queue.size());
+        assertFalse(queue.contains("drop"));
+
+        queue.add("added");
+        List<String> seen = new ArrayList<>();
+        for (String element : queue) {
+            seen.add(element);
+        }
+        assertEquals(2, seen.size(), "the iterator must see both queued 
elements");
+        assertTrue(seen.contains("added"));
+    }
+
+    @Test
+    void takerBlocksUntilElementArrives() throws Exception {
+        final FairBlockingQueue<String> queue = new FairBlockingQueue<>();
+        final AtomicReference<String> taken = new AtomicReference<>();
+        final CountDownLatch done = new CountDownLatch(1);
+        Thread taker = new Thread(() -> {
+            try {
+                taken.set(queue.take());
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } finally {
+                done.countDown();
+            }
+        });
+        taker.start();
+        Thread.sleep(100);
+        queue.offer("late");
+        assertTrue(done.await(5, TimeUnit.SECONDS), "taker must be handed the 
element");
+        assertEquals("late", taken.get());
+        taker.join(1000);
+    }
+
+    @Test
+    void handoffIsFairToLongestWaitingConsumer() throws Exception {
+        final FairBlockingQueue<String> queue = new FairBlockingQueue<>();
+        final AtomicReference<String> firstIn = new AtomicReference<>();
+        final AtomicReference<String> secondIn = new AtomicReference<>();
+        final CountDownLatch firstDone = new CountDownLatch(1);
+        final CountDownLatch secondDone = new CountDownLatch(1);
+
+        Thread first = new Thread(() -> {
+            try {
+                firstIn.set(queue.poll(10, TimeUnit.SECONDS));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } finally {
+                firstDone.countDown();
+            }
+        });
+        Thread second = new Thread(() -> {
+            // ensure the first waiter registered first
+            try {
+                Thread.sleep(200);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            try {
+                secondIn.set(queue.poll(10, TimeUnit.SECONDS));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } finally {
+                secondDone.countDown();
+            }
+        });
+        first.start();
+        Thread.sleep(50); // first waiter is queued first
+        second.start();
+        Thread.sleep(300); // both waiting
+
+        queue.offer("one");
+        queue.offer("two");
+
+        assertTrue(firstDone.await(5, TimeUnit.SECONDS), "first waiter must be 
served");
+        assertTrue(secondDone.await(5, TimeUnit.SECONDS), "second waiter must 
be served");
+        assertSame("one", firstIn.get(),
+                "the longest waiting consumer must receive the first element, 
got "
+                        + firstIn.get());
+        assertSame("two", secondIn.get());
+    }
+
+    @Test
+    void putAndAddAllFillAnUnboundedQueue() throws Exception {
+        FairBlockingQueue<String> queue = new FairBlockingQueue<>();
+        int count = 50;
+        for (int i = 0; i < count; i++) {
+            queue.put("e" + i);
+        }
+        assertEquals(count, queue.size());
+        assertEquals(Integer.MAX_VALUE - count, queue.remainingCapacity());
+
+        List<String> drained = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            drained.add(queue.take());
+        }
+        assertEquals("e" + (count - 1), drained.get(0),
+                "blocking take must follow the same most-recently-used order");
+        assertEquals(0, queue.size());
+        assertTrue(queue.isEmpty());
+    }
+
+    @Test
+    void bulkCollectionOperationsAreUnsupportedByContract() {
+        FairBlockingQueue<String> queue = new FairBlockingQueue<>();
+        queue.offer("item");
+        assertThrows(UnsupportedOperationException.class, queue::clear);
+        assertThrows(UnsupportedOperationException.class,
+                () -> queue.containsAll(Collections.singletonList("item")));
+        assertThrows(UnsupportedOperationException.class,
+                () -> queue.removeAll(Collections.emptyList()));
+        assertThrows(UnsupportedOperationException.class,
+                () -> queue.retainAll(Collections.emptyList()));
+        assertThrows(UnsupportedOperationException.class,
+                () -> queue.drainTo(new ArrayList<>()));
+        assertEquals(1, queue.size(),
+                "the unsupported operations must not mutate the queue");
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/InterceptorSemanticsTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/InterceptorSemanticsTest.java
new file mode 100644
index 0000000000..89bcd8cd53
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/InterceptorSemanticsTest.java
@@ -0,0 +1,179 @@
+/*
+ *  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.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+import org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport;
+
+/**
+ * Expected functionality of the interceptor chain, exercised against
+ * the real database: slow/failed query reporting, statement timeout
+ * cancellation, per-connection statement caching and the decorated
+ * statement proxy contract.
+ */
+class InterceptorSemanticsTest extends PoolTestSupport {
+
+    /** Heavy pure-SQL aggregate; H2 honours query timeouts on it (~2 s). */
+    private static final String HEAVY_SQL_PREFIX =
+            "SELECT COUNT(*) FROM SYSTEM_RANGE(1,400000000) r1, 
SYSTEM_RANGE(1,3)";
+
+    @Test
+    void slowQueryIsRecordedInPoolStats() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJdbcInterceptors(
+                
"org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport(threshold=100)");
+        ds.createPool();
+        String poolName = ds.getPoolProperties().getPoolName();
+
+        String nonce = "nonce_" + System.nanoTime();
+        String sql = HEAVY_SQL_PREFIX + " /* " + nonce + " */";
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            assertTrue(st.execute(sql));
+        }
+
+        ConcurrentHashMap<String, ?> stats = 
SlowQueryReport.getPoolStats(poolName);
+        assertNotNull(stats, "stats must exist once a slow query was run");
+        assertTrue(statsKeySet(stats).stream().anyMatch(k -> 
k.contains(nonce)),
+                "slow query must be reported under pool " + poolName
+                        + ", recorded keys: " + statsKeySet(stats));
+    }
+
+    @Test
+    void failedQueryIsRecordedInPoolStats() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJdbcInterceptors(
+                "org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport");
+        ds.createPool();
+        String poolName = ds.getPoolProperties().getPoolName();
+
+        String nonce = "fail_" + System.nanoTime();
+        String sql = "SELECT * FROM no_such_table_" + nonce;
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            assertThrows(SQLException.class, () -> st.executeQuery(sql));
+        }
+
+        ConcurrentHashMap<String, ?> stats = 
SlowQueryReport.getPoolStats(poolName);
+        assertNotNull(stats, "stats must exist once a query failed");
+        assertTrue(statsKeySet(stats).stream().anyMatch(k -> 
k.contains(nonce)),
+                "failed query must be reported under pool " + poolName
+                        + ", recorded keys: " + statsKeySet(stats));
+    }
+
+    private static java.util.Set<String> statsKeySet(ConcurrentHashMap<String, 
?> stats) {
+        return stats.keySet();
+    }
+
+    @Test
+    void queryTimeoutInterceptorCancelsAndPoolStaysUsable() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJdbcInterceptors(
+                
"org.apache.tomcat.jdbc.pool.interceptor.QueryTimeoutInterceptor(queryTimeout=1)");
+        ds.getPoolProperties().setMaxActive(1);
+
+        long start = System.currentTimeMillis();
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            assertThrows(SQLException.class, () -> 
st.execute(HEAVY_SQL_PREFIX));
+        }
+        long elapsed = System.currentTimeMillis() - start;
+        assertTrue(elapsed >= 900 && elapsed < 8000,
+                "query must be cancelled around the configured timeout, took "
+                        + elapsed + " ms");
+
+        // the pool and its physical connection must survive the cancellation
+        assertEquals(1, poolSessions());
+        try (Connection c = ds.getConnection();
+                ResultSet rs = c.createStatement().executeQuery("SELECT 1")) {
+            assertTrue(rs.next());
+        }
+    }
+
+    @Test
+    void closedStatementFailsWithoutCacheControl() throws Exception {
+        DataSource ds = newDataSource(); // no interceptors
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection()) {
+            c.createStatement().execute("CREATE TABLE " + t + " (v INT)");
+            PreparedStatement ps = c.prepareStatement("INSERT INTO " + t + " 
VALUES (1)");
+            assertEquals(1, ps.executeUpdate());
+            ps.close();
+            assertThrows(SQLException.class, ps::executeUpdate,
+                    "a physically closed statement must fail");
+        }
+    }
+
+    @Test
+    void statementCacheServesRepeatedPreparesWithinOneBorrow() throws 
Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJdbcInterceptors(
+                
"org.apache.tomcat.jdbc.pool.interceptor.StatementCache(maxCacheSize=10)");
+        ds.getPoolProperties().setMaxActive(1);
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection()) {
+            c.createStatement().execute("CREATE TABLE " + t + " (v INT)");
+            String sql = "INSERT INTO " + t + " VALUES (1)";
+
+            PreparedStatement first = c.prepareStatement(sql);
+            assertEquals(1, first.executeUpdate());
+            first.close(); // returned to the cache, not physically closed
+
+            PreparedStatement second = c.prepareStatement(sql);
+            assertEquals(1, second.executeUpdate(),
+                    "the cached statement must be usable again after close");
+            second.close();
+        }
+        assertEquals(2, adminRowCount(t));
+    }
+
+    @Test
+    void decoratedStatementProxyEnforcesClosedContract() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJdbcInterceptors(
+                
"org.apache.tomcat.jdbc.pool.interceptor.StatementDecoratorInterceptor");
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection()) {
+            c.createStatement().execute("CREATE TABLE " + t + " (v INT)");
+
+            Statement st = c.createStatement();
+            assertFalse(st.isClosed());
+            try (ResultSet rs = st.executeQuery("SELECT v FROM " + t)) {
+                assertFalse(rs.next(), "empty table");
+            }
+            st.close();
+            assertTrue(st.isClosed());
+            assertThrows(SQLException.class,
+                    () -> st.execute("INSERT INTO " + t + " VALUES (1)"),
+                    "closed decorated statement must reject execution");
+        }
+        assertEquals(0, adminRowCount(t),
+                "execution attempted after close must not reach the database");
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/JmxMonitorTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/JmxMonitorTest.java
new file mode 100644
index 0000000000..58220cdf00
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/JmxMonitorTest.java
@@ -0,0 +1,214 @@
+/*
+ *  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.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.management.ManagementFactory;
+import java.sql.Connection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+
+/**
+ * Expected JMX monitoring functionality. A container registers the
+ * DataSource as an MBean, which drives the {@code MBeanRegistration}
+ * callbacks {@code preRegister} and {@code postDeregister}; those publish
+ * the pool's own MBean under the {@code tomcat.jdbc} domain, and each
+ * physical connection publishes itself below that name. The tests invoke
+ * those same container callbacks and then read the published statistics
+ * while real traffic flows to a real database.
+ */
+class JmxMonitorTest extends PoolTestSupport {
+
+    private static final String POOL_DOMAIN = "tomcat.jdbc";
+
+    private static long attr(MBeanServer server, ObjectName name, String 
attribute)
+            throws Exception {
+        return ((Number) server.getAttribute(name, attribute)).longValue();
+    }
+
+    private static ObjectName uniqueDataSourceName(String role)
+            throws MalformedObjectNameException {
+        return new ObjectName("tomcat:type=FunctionalTestDataSource,role=" + 
role
+                + ",uid=" + System.nanoTime());
+    }
+
+    private static Set<ObjectName> poolMBeans(MBeanServer server) throws 
Exception {
+        return server.queryNames(new ObjectName(POOL_DOMAIN + ":*"), null);
+    }
+
+    /**
+     * Publishes an already created pool the way a container does and
+     * returns the single pool MBean that appeared, or {@code null} if
+     * nothing was published.
+     */
+    private ObjectName registerAndFindPoolMBean(MBeanServer server, DataSource 
ds,
+            ObjectName dsName) throws Exception {
+        Set<ObjectName> before = poolMBeans(server);
+        ds.preRegister(server, dsName);
+        Set<ObjectName> added = new HashSet<>(poolMBeans(server));
+        added.removeAll(before);
+        return added.isEmpty() ? null : added.iterator().next();
+    }
+
+    /** Counts the connection MBeans published below the given pool MBean. */
+    private static int connectionMBeanCount(MBeanServer server, ObjectName 
poolOname)
+            throws Exception {
+        // the connection MBeans repeat the pool's key properties and add
+        // "connections=PooledConnection[n]", so match on those keys rather
+        // than on a string prefix (canonical names sort the properties)
+        String uid = poolOname.getKeyProperty("uid");
+        int count = 0;
+        for (ObjectName name : poolMBeans(server)) {
+            if (name.getKeyProperty("connections") != null
+                    && uid.equals(name.getKeyProperty("uid"))) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    @Test
+    void poolMBeanTracksLiveTraffic() throws Exception {
+        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJmxEnabled(true);
+        ds.createPool();
+
+        ObjectName dsName = uniqueDataSourceName("live");
+        ObjectName poolOname = registerAndFindPoolMBean(server, ds, dsName);
+        try {
+            assertNotNull(poolOname, "an enabled pool must publish its MBean");
+            assertEquals(POOL_DOMAIN, poolOname.getDomain(),
+                    "the pool MBean must be published under the tomcat.jdbc 
domain");
+            assertEquals("ConnectionPool", poolOname.getKeyProperty("type"));
+            assertEquals(DataSource.class.getName(), 
poolOname.getKeyProperty("class"));
+
+            // the fixture pre-creates one physical connection
+            assertEquals(0, attr(server, poolOname, "NumActive"));
+            assertEquals(1, attr(server, poolOname, "NumIdle"));
+
+            Connection borrowed = ds.getConnection();
+            assertEquals(1, attr(server, poolOname, "NumActive"),
+                    "NumActive must reflect the borrowed connection");
+            assertEquals(0, attr(server, poolOname, "NumIdle"));
+            borrowed.close();
+            assertEquals(0, attr(server, poolOname, "NumActive"));
+            assertEquals(1, attr(server, poolOname, "NumIdle"),
+                    "NumIdle must reflect the returned connection");
+
+            assertTrue(attr(server, poolOname, "BorrowedCount") >= 1,
+                    "BorrowedCount must count borrows");
+            assertTrue(attr(server, poolOname, "ReturnedCount") >= 1,
+                    "ReturnedCount must count returns");
+        } finally {
+            ds.postDeregister();
+            assertFalse(server.isRegistered(poolOname),
+                    "postDeregister must unregister the pool MBean");
+        }
+    }
+
+    @Test
+    void connectionMBeansFollowThePhysicalConnections() throws Exception {
+        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJmxEnabled(true);
+        ds.getPoolProperties().setMaxActive(2);
+        ds.createPool();
+
+        ObjectName dsName = uniqueDataSourceName("connections");
+        Set<ObjectName> before = poolMBeans(server);
+        ObjectName poolOname = registerAndFindPoolMBean(server, ds, dsName);
+        try {
+            assertNotNull(poolOname);
+            Connection first = ds.getConnection();
+            Connection second = ds.getConnection();
+            assertEquals(2, connectionMBeanCount(server, poolOname),
+                    "every physical connection in use must be published below 
the pool");
+
+            first.close();
+            second.close();
+            assertEquals(2, connectionMBeanCount(server, poolOname),
+                    "returned connections stay published, they are still 
pooled");
+        } finally {
+            ds.close(); // releases the physical connections
+            ds.postDeregister();
+            Set<ObjectName> leftovers = new HashSet<>(poolMBeans(server));
+            leftovers.removeAll(before);
+            assertTrue(leftovers.isEmpty(),
+                    "closing the pool must unregister every MBean it 
published, left: "
+                            + leftovers);
+        }
+    }
+
+    @Test
+    void monitoredConnectionsStayFullyFunctional() throws Exception {
+        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJmxEnabled(true);
+        ds.createPool();
+
+        ObjectName dsName = uniqueDataSourceName("monitor");
+        ObjectName poolOname = registerAndFindPoolMBean(server, ds, dsName);
+        String table = uniqueTable();
+        try {
+            assertNotNull(poolOname, "an enabled pool must publish its MBean");
+            try (Connection c = ds.getConnection()) {
+                assertEquals("H2", c.getMetaData().getDatabaseProductName(),
+                        "monitoring must not interfere with the connection");
+                c.createStatement().execute("CREATE TABLE " + table + " (v 
INT)");
+                assertEquals(1, c.createStatement().executeUpdate("INSERT INTO 
" + table
+                        + " VALUES (1)"));
+            }
+            assertEquals(0, attr(server, poolOname, "NumActive"));
+            assertEquals(1, adminRowCount(table),
+                    "work done on a monitored connection must reach the 
database");
+        } finally {
+            ds.postDeregister();
+        }
+    }
+
+    @Test
+    void disabledJmxPublishesNothing() throws Exception {
+        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+        DataSource ds = newDataSource(); // the fixture disables JMX
+        ds.createPool();
+
+        ObjectName dsName = uniqueDataSourceName("disabled");
+        Set<ObjectName> before = poolMBeans(server);
+        ds.preRegister(server, dsName);
+        try {
+            Set<ObjectName> added = new HashSet<>(poolMBeans(server));
+            added.removeAll(before);
+            assertTrue(added.isEmpty(),
+                    "a JMX-disabled pool must not publish MBeans, added: " + 
added);
+        } finally {
+            ds.postDeregister();
+        }
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/NamingFactoryTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/NamingFactoryTest.java
new file mode 100644
index 0000000000..725f7dc5bf
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/NamingFactoryTest.java
@@ -0,0 +1,137 @@
+/*
+ *  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.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.naming.Name;
+import javax.naming.Reference;
+import javax.naming.StringRefAddr;
+import javax.naming.CompositeName;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+import org.apache.tomcat.jdbc.pool.DataSourceFactory;
+import org.apache.tomcat.jdbc.pool.XADataSource;
+
+/**
+ * Expected JNDI/naming factory functionality: a container-style
+ * {@code Resource} description (a {@link Reference}) produces a
+ * correctly configured, fully working pool; and the generic naming
+ * factory applies documented property types to the configured object.
+ */
+class NamingFactoryTest extends PoolTestSupport {
+
+    private Reference dataSourceReference(String className) {
+        Reference ref = new Reference(className, null, null);
+        ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"));
+        ref.add(new StringRefAddr("url", getUrl()));
+        ref.add(new StringRefAddr("username", "sa"));
+        ref.add(new StringRefAddr("password", ""));
+        ref.add(new StringRefAddr("maxActive", "4"));
+        ref.add(new StringRefAddr("initialSize", "1"));
+        ref.add(new StringRefAddr("minIdle", "0"));
+        return ref;
+    }
+
+    @Test
+    void referenceProducesWorkingConfiguredPool() throws Exception {
+        Name name = new CompositeName("jdbc/FunctionalDS");
+        Object result = new DataSourceFactory().getObjectInstance(
+                dataSourceReference("javax.sql.DataSource"), name, null, null);
+
+        assertInstanceOf(DataSource.class, result,
+                "factory must produce a pool DataSource");
+        DataSource ds = manage((DataSource) result);
+        assertEquals(4, ds.getPoolProperties().getMaxActive(),
+                "maxActive from the reference must be applied");
+        assertEquals("sa", ds.getPoolProperties().getUsername());
+
+        try (java.sql.Connection c = ds.getConnection();
+                java.sql.ResultSet rs = 
c.createStatement().executeQuery("SELECT 1")) {
+            assertTrue(rs.next());
+        }
+        assertEquals(1, poolSessions(),
+                "the JNDI-configured pool must reach the real database");
+    }
+
+    @Test
+    void xaReferenceProducesXaDataSourceType() throws Exception {
+        Name name = new CompositeName("jdbc/FunctionalXaDS");
+        Object result = new DataSourceFactory().getObjectInstance(
+                dataSourceReference("javax.sql.XADataSource"), name, null, 
null);
+        assertInstanceOf(XADataSource.class, result,
+                "a javax.sql.XADataSource reference must yield the pool's XA 
type");
+        if (result instanceof DataSource) {
+            manage((DataSource) result);
+        }
+    }
+
+    @Test
+    void namingFactoryAppliesDocumentedPropertyTypes() throws Exception {
+        Reference ref = new Reference(NamingTarget.class.getName(), null, 
null);
+        ref.add(new StringRefAddr("name", "configured"));
+        ref.add(new StringRefAddr("count", "7"));
+        ref.add(new StringRefAddr("ratio", "1234567890"));
+        ref.add(new StringRefAddr("enabled", "true"));
+
+        Object result = new 
org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory()
+                .getObjectInstance(ref, new CompositeName("test/target"), 
null, null);
+        assertInstanceOf(NamingTarget.class, result);
+        NamingTarget target = (NamingTarget) result;
+        assertEquals("configured", target.getName());
+        assertEquals(7, target.getCount());
+        assertEquals(1234567890L, target.getRatio());
+        assertTrue(target.isEnabled());
+    }
+
+    /** Plain configuration target for the generic naming factory. */
+    public static class NamingTarget {
+        private String name;
+        private int count;
+        private long ratio;
+        private boolean enabled;
+
+        public String getName() {
+            return name;
+        }
+        public void setName(String name) {
+            this.name = name;
+        }
+        public int getCount() {
+            return count;
+        }
+        public void setCount(int count) {
+            this.count = count;
+        }
+        public long getRatio() {
+            return ratio;
+        }
+        public void setRatio(long ratio) {
+            this.ratio = ratio;
+        }
+        public boolean isEnabled() {
+            return enabled;
+        }
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolTestSupport.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolTestSupport.java
index f2145ec631..f0d47a5d80 100644
--- 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolTestSupport.java
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolTestSupport.java
@@ -102,6 +102,11 @@ public abstract class PoolTestSupport {
         ds.getPoolProperties().setJmxEnabled(false);
         ds.getPoolProperties().setInitialSize(1);
         ds.getPoolProperties().setMinIdle(0);
+        return manage(ds);
+    }
+
+    /** Registers an externally constructed DataSource for teardown closing. */
+    protected <D extends DataSource> D manage(D ds) {
         createdDataSources.add(ds);
         return ds;
     }
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/XaConnectionTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/XaConnectionTest.java
new file mode 100644
index 0000000000..1ec24ac07b
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/XaConnectionTest.java
@@ -0,0 +1,173 @@
+/*
+ *  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.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.XAConnection;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+import org.apache.tomcat.jdbc.pool.XADataSource;
+
+/**
+ * Expected XA functionality: with a real XA-capable data source
+ * injected (H2's {@code org.h2.jdbcx.JdbcDataSource}), the pool hands
+ * out pooled connections that implement {@link XAConnection}, and the
+ * XA resource drives real two-phase commit against the database.
+ */
+class XaConnectionTest extends PoolTestSupport {
+
+    /** Simple test transaction id. */
+    private static final class TestXid implements Xid {
+        private final byte qualifier;
+        TestXid(int qualifier) {
+            this.qualifier = (byte) qualifier;
+        }
+        @Override
+        public int getFormatId() {
+            return 1;
+        }
+        @Override
+        public byte[] getGlobalTransactionId() {
+            return new byte[] { 42, qualifier };
+        }
+        @Override
+        public byte[] getBranchQualifier() {
+            return new byte[] { qualifier };
+        }
+    }
+
+    private XADataSource newXaPool() {
+        org.h2.jdbcx.JdbcDataSource inner = new org.h2.jdbcx.JdbcDataSource();
+        inner.setUrl(getUrl());
+        inner.setUser("sa");
+        inner.setPassword("");
+        XADataSource ds = new XADataSource();
+        ds.setDriverClassName("org.h2.Driver");
+        ds.getPoolProperties().setJmxEnabled(false);
+        ds.getPoolProperties().setInitialSize(1);
+        ds.getPoolProperties().setMinIdle(0);
+        ds.getPoolProperties().setDataSource(inner);
+        return manage(ds);
+    }
+
+    @Test
+    void xaCommitPersistsThroughTheDatabase() throws Exception {
+        XADataSource ds = newXaPool();
+        String t = uniqueTable();
+
+        XAConnection xc = ds.getXAConnection();
+        assertNotNull(xc);
+        XAResource xa = xc.getXAResource();
+        assertNotNull(xa, "pooled connection must expose its XA resource");
+        Xid xid = new TestXid(1);
+
+        Connection c = xc.getConnection();
+        c.setAutoCommit(false);
+        xa.start(xid, XAResource.TMNOFLAGS);
+        try (Statement st = c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (v INT)");
+            st.execute("INSERT INTO " + t + " VALUES (1)");
+        }
+        xa.end(xid, XAResource.TMSUCCESS);
+        assertEquals(XAResource.XA_OK, xa.prepare(xid));
+
+        // not visible before commit
+        assertEquals(0, adminRowCount(t), "work must be invisible before 
commit");
+
+        xa.commit(xid, false);
+        assertEquals(1, adminRowCount(t), "XA commit must persist to the 
database");
+
+        // the branch is complete; returning the XA connection to the pool
+        // must keep the physical connection alive for the next borrower
+        xc.close();
+        assertEquals(1, poolSessions(),
+                "the physical XA connection must stay in the pool");
+        try (Connection again = ds.getConnection();
+                ResultSet rs = again.createStatement().executeQuery("SELECT 
1")) {
+            assertTrue(rs.next(), "the pooled connection must still be 
usable");
+        }
+    }
+
+    @Test
+    void xaRollbackDiscardsWorkAndConnectionIsReusable() throws Exception {
+        XADataSource ds = newXaPool();
+        String t = uniqueTable();
+
+        XAConnection xc = ds.getXAConnection();
+        XAResource xa = xc.getXAResource();
+        Xid xid = new TestXid(2);
+
+        Connection c = xc.getConnection();
+        c.setAutoCommit(false);
+        xa.start(xid, XAResource.TMNOFLAGS);
+        try (Statement st = c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (v INT)");
+            st.execute("INSERT INTO " + t + " VALUES (1)");
+        }
+        xa.end(xid, XAResource.TMSUCCESS);
+        xa.rollback(xid);
+        assertEquals(0, adminRowCount(t), "rolled back XA work must be gone");
+        xc.close();
+
+        // pool hands the very same physical connection out again, for plain
+        // non-XA use, with the session state restored to the pool defaults
+        try (Connection again = ds.getConnection()) {
+            try (ResultSet rs = again.createStatement().executeQuery("SELECT 
1")) {
+                assertTrue(rs.next());
+            }
+            assertTrue(again.getAutoCommit(),
+                    "a returned XA connection must come back in auto commit 
mode");
+            setSessionMarker(again, 7);
+        }
+        try (Connection third = ds.getConnection()) {
+            assertEquals(Integer.valueOf(7), sessionMarker(third),
+                    "the rolled back XA connection must have been pooled, not 
replaced");
+        }
+        assertEquals(1, poolSessions(),
+                "the XA pool must hold a single physical connection");
+    }
+
+    @Test
+    void xaPoolHandedOutConnectionImplementsXaConnection() throws Exception {
+        XADataSource ds = newXaPool();
+        Connection c = ds.getConnection();
+        assertInstanceOf(XAConnection.class, c,
+                "connections from an XA pool must implement XAConnection");
+        c.close();
+    }
+
+    @Test
+    void plainPoolRefusesXaConnections() throws Exception {
+        DataSource ds = newDataSource(); // non-XA pool
+        assertThrows(SQLException.class, ds::getXAConnection,
+                "a non-XA pool must refuse XA connections");
+    }
+}


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

Reply via email to