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]
