This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit a1c7298b007465b4bfa6906ed8ff3095e8b583d8 Author: opencode <[email protected]> AuthorDate: Thu Nov 5 13:13:37 2026 +0100 Run the connection MBean sweep outside the DataSource monitor DataSource.registerJmx() was synchronized and published the pool MBean and then swept every pooled connection while still holding the DataSource monitor. The sweep takes each connection's lock, which pool maintenance holds across validation and reconnect driver calls, so a JMX registration could park close and first-borrow publication behind an unresponsive database. Split the publication into publishJmx(), which runs under the monitor, and run registerConnectionMBeans() after releasing it. Update the registration-vs-close test seam to hook publishJmx() and add a test that a lifecycle callback completes while the sweep is parked on a held connection lock. --- .../org/apache/tomcat/jdbc/pool/DataSource.java | 98 ++++++++++++++-------- .../apache/tomcat/jdbc/pool/mbeans-descriptors.xml | 14 ++-- .../jmx/TestDataSourceRegistrationVsClose.java | 77 ++++++++++++++++- 3 files changed, 147 insertions(+), 42 deletions(-) diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java index 6950f51695..27dd5c4079 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSource.java @@ -33,6 +33,10 @@ import org.apache.tomcat.jdbc.pool.util.MessageFactory; * A DataSource that can be instantiated through IoC and implements the DataSource interface since the DataSourceProxy * is used as a generic proxy. The DataSource simply wraps a {@link ConnectionPool} in order to provide a standard * interface to the user. + * <p> + * A JMX registration made before the pool exists publishes nothing, because there is no pool MBean to publish. Create + * the pool before registering the data source, or register it again after the pool has been created, to have the pool + * monitored. */ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, MBeanRegistration, org.apache.tomcat.jdbc.pool.jmx.ConnectionPoolMBean, javax.sql.ConnectionPoolDataSource { @@ -85,8 +89,6 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, closePool(p, all); } - // The JMX operations below register the pool itself under the - // tomcat.jdbc domain. /** * The JMX ObjectName for the connection pool MBean. The handle is checked, * cleared and released only under this data source's monitor. It is written @@ -113,7 +115,7 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, /** * The name derived from the registration currently in flight, set and * cleared by {@link #preRegister(MBeanServer, ObjectName)} around its call - * into {@link #registerJmx()}, under this data source's monitor. A second + * into {@link #publishJmx()}, under this data source's monitor. A second * registration must read its publish target from here rather than from the * handle slot, so it can never overwrite the handle of a publish that is * already held. @@ -164,7 +166,6 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, */ @Override public void preDeregister() throws Exception { - // NOOP } /** @@ -174,27 +175,34 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, * {@inheritDoc} */ @Override - public synchronized ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { - // Under the monitor close() takes, so the publication below cannot - // overlap a closure: a close that ran completely between registerJmx() - // reading the registration name and the server actually accepting the - // publish would unregister a name the pool never took and close the - // pool, and the publish would then register the dead pool's MBean - // under a live name that no later close() or postDeregister() - which - // only release a held handle - could ever take back. + public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { + // The publish runs under the monitor close() takes, so it cannot + // overlap a closure: a close that ran completely between the name + // being derived and the server accepting the publish would unregister + // a name the pool never took and close the pool, and the publish would + // then register the dead pool's MBean under a live name that no later + // close() or postDeregister() - which only release a held handle - + // could ever take back. The connection sweep that follows the publish + // runs outside the monitor: the sweep takes each connection's lock, + // which maintenance holds across validation and reconnect driver + // calls, so holding the data source monitor across it would park a + // close or a later publication behind a hung database. + ConnectionPool sweepPool = null; try { - if (isJmxEnabled() && oname == null) { - final ObjectName derived = createObjectName(name); - if (derived != null) { - try { + synchronized (this) { + if (isJmxEnabled() && oname == null) { + final ObjectName derived = createObjectName(name); + if (derived != null) { pendingName = derived; - registerJmx(); - } finally { - pendingName = null; + try { + sweepPool = publishJmx(); + } finally { + pendingName = null; + } } + } else if (isJmxEnabled() && oname != null) { + log.warn(MessageFactory.get("dataSource.mbeanAlreadyRegistered", oname, name)); } - } else if (isJmxEnabled() && oname != null) { - log.warn(MessageFactory.get("dataSource.mbeanAlreadyRegistered", oname, name)); } } catch (MalformedObjectNameException x) { // Nothing has been published and the handle slot was not touched: @@ -205,12 +213,19 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, // pool-side state (for instance a pool that has not been // created yet); preRegister throwing aborts the whole // registration in the MBean server. The handle slot is left as - // registerJmx() last wrote it: a RuntimeException after a + // publishJmx() last wrote it: a RuntimeException after a // completed publish - from a later step than the publish itself - // must not lose the handle of a live MBean, whose release then // belongs to the close and the deregistration callbacks as ever. log.error(MessageFactory.get("dataSource.unableToRegisterMBean"), x); } + if (sweepPool != null) { + try { + sweepPool.registerConnectionMBeans(); + } catch (RuntimeException x) { + log.error(MessageFactory.get("dataSource.unableToRegisterMBean"), x); + } + } return name; } @@ -247,10 +262,25 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, * already holding disturbed: it names a publish that is still in effect, and dropping it here would leave that * MBean held by nothing - neither a later close() nor a deregistration could ever take it back. */ - protected synchronized void registerJmx() { - // Runs under this data source's monitor - from preRegister() or a - // direct call - the monitor close(boolean) also takes, so no closure - // can interleave with the publish below. + protected void registerJmx() { + final ConnectionPool p; + synchronized (this) { + p = publishJmx(); + } + if (p != null) { + p.registerConnectionMBeans(); + } + } + + /** + * Publishes the pool MBean under this data source's monitor and returns + * the pool whose connections must then be published, or {@code null} when + * nothing was published. The caller must hold this monitor; the + * connection sweep is the caller's to run outside it. + * + * @return the pool whose connections need publishing, or {@code null} + */ + protected ConnectionPool publishJmx() { final ConnectionPool p = pool; // Read the pool's JMX object only once: a pool that was closed before // this registration has given it up, and one read that saw it must not @@ -268,13 +298,13 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, // Nothing to publish; the handle slot, which this call did not // write, is left as it stands. log.warn(MessageFactory.get("dataSource.mbeanNotRegistered", name)); - return; + return null; } final ObjectName published = JmxUtil.registerJmx(name, null, jmx); if (published != null) { - // Recorded before the connection sweep below: a publish the server - // accepted is held from the moment it happened, whatever a later - // step makes of it. + // Recorded before the caller runs the connection sweep: a publish + // the server accepted is held from the moment it happened, whatever + // a later step makes of it. this.oname = published; // Only a registration-driven publish - one made from // preRegister(), which sets pendingName around its call - has a @@ -286,10 +316,12 @@ public class DataSource extends DataSourceProxy implements javax.sql.DataSource, // A connection publishes itself below this name when its physical // connection is established; the connections the pool was already // holding could not do that while this name did not exist yet, so - // they are published now. A publish the server refused registers - // nothing of its own and has no connections to publish either. - p.registerConnectionMBeans(); + // the caller publishes them now. A publish the server refused + // registers nothing of its own and has no connections to publish + // either. + return p; } + return null; } /** diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml index be6ba5a585..1e78ab914d 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/mbeans-descriptors.xml @@ -32,12 +32,12 @@ writeable="false"/> <attribute name="size" - description="The number of established connections in the pool, idle and in use" + description="The current pool size: idle and in-use connection slots, including wake-up placeholders for waiting borrowers" type="java.lang.Integer" writeable="false"/> <attribute name="idle" - description="The number of established connections in the pool that are idle" + description="The number of idle entries, including wake-up placeholders for waiting borrowers" type="java.lang.Integer" writeable="false"/> @@ -47,7 +47,7 @@ writeable="false"/> <attribute name="active" - description="The number of established connections in the pool that are in use" + description="The number of connections currently in use by borrowers" type="java.lang.Integer" writeable="false"/> @@ -57,7 +57,7 @@ writeable="false"/> <attribute name="poolSweeperEnabled" - description="Returns true if the pool has a background thread running" + description="True if the pool configuration enables the background sweeper" type="java.lang.Boolean" is="true" writeable="false"/> @@ -95,7 +95,7 @@ writeable="false"/> <attribute name="defaultCatalog" - description="The JDBC transaction isolation setting for new connections" + description="The default JDBC catalog for new connections" type="java.lang.String" writeable="false"/> @@ -191,7 +191,7 @@ writeable="false"/> <attribute name="loginTimeout" - description="Not in use" + description="The login timeout in seconds, mapped to the pool's maxWait" type="java.lang.Integer" writeable="false"/> @@ -385,7 +385,7 @@ returnType="void" /> <operation name="testIdle" - description="forces a validation of abandoned connections" + description="Runs a validation and eviction pass over the idle connections" impact="ACTION" returnType="void" /> diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/jmx/TestDataSourceRegistrationVsClose.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/jmx/TestDataSourceRegistrationVsClose.java index 90758004e0..0398278011 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/jmx/TestDataSourceRegistrationVsClose.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/jmx/TestDataSourceRegistrationVsClose.java @@ -30,7 +30,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.apache.tomcat.jdbc.JdbcPoolBaseTest; +import org.apache.tomcat.jdbc.pool.ConnectionPool; import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PooledConnection; /** * The {@code DataSource}'s JMX lifecycle - {@code preRegister()}, @@ -88,6 +90,7 @@ class TestDataSourceRegistrationVsClose extends JdbcPoolBaseTest { private final AtomicInteger releases = new AtomicInteger(); private volatile CountDownLatch publicationEntered; private volatile CountDownLatch publicationGate = new CountDownLatch(0); + private volatile CountDownLatch publicationDone; private volatile boolean publicationUnderMonitor; private volatile CountDownLatch releaseEntered; private volatile CountDownLatch releaseGate = new CountDownLatch(0); @@ -99,7 +102,7 @@ class TestDataSourceRegistrationVsClose extends JdbcPoolBaseTest { } @Override - protected void registerJmx() { + protected ConnectionPool publishJmx() { publicationUnderMonitor = Thread.holdsLock(this); CountDownLatch entered = publicationEntered; if (entered != null) { @@ -107,7 +110,13 @@ class TestDataSourceRegistrationVsClose extends JdbcPoolBaseTest { entered.countDown(); awaitGate(publicationGate); } - super.registerJmx(); + ConnectionPool published = super.publishJmx(); + CountDownLatch done = publicationDone; + if (done != null) { + publicationDone = null; + done.countDown(); + } + return published; } @Override @@ -286,6 +295,70 @@ class TestDataSourceRegistrationVsClose extends JdbcPoolBaseTest { } } + @Test + void theConnectionSweepDoesNotHoldTheDataSourceMonitor() throws Exception { + MBeanServer server = server(); + SeamDataSource ds = newSeamDataSource(); + ds.createPool(); + ObjectName dsName = uniqueName("sweep-vs-monitor"); + + PooledConnection held; + try (java.sql.Connection connection = ds.getConnection()) { + held = connection.unwrap(PooledConnection.class); + } + // The connection is idle now; hold its lock so the registration's + // connection sweep parks on it. + held.lock(); + ds.publicationDone = new CountDownLatch(1); + AtomicReference<Throwable> registrationFailure = new AtomicReference<>(); + Thread registrar = new Thread(() -> { + try { + server.registerMBean(new TestJmxRegistration.Container(ds), dsName); + } catch (Throwable x) { + registrationFailure.set(x); + } + }, "registration parked in sweep"); + registrar.setDaemon(true); + try { + registrar.start(); + Assertions.assertTrue(ds.publicationDone.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "the registration must have published the pool MBean"); + + // The publication is done and the sweep is now stuck on the + // connection lock. A lifecycle callback that touches no connection + // must not be behind that sweep. + AtomicBoolean callbackDone = new AtomicBoolean(); + AtomicReference<Throwable> callbackFailure = new AtomicReference<>(); + Thread callback = new Thread(() -> { + try { + ds.postRegister(Boolean.TRUE); + } catch (Throwable x) { + callbackFailure.set(x); + } finally { + callbackDone.set(true); + } + }, "lifecycle during sweep"); + callback.setDaemon(true); + callback.start(); + callback.join(2000); + Assertions.assertTrue(callbackDone.get(), + "the connection sweep must run outside the data source monitor: a " + + "lifecycle callback was parked behind a sweep waiting on a " + + "connection lock"); + Assertions.assertNull(callbackFailure.get(), "the lifecycle callback must not fail"); + } finally { + held.unlock(); + registrar.join(AWAIT_TIMEOUT_MS); + if (server.isRegistered(dsName)) { + server.unregisterMBean(dsName); + } + ds.close(); + sweep(server, dsName); + } + Assertions.assertFalse(registrar.isAlive(), "the registration must complete once the connection is released"); + Assertions.assertNull(registrationFailure.get(), "the registration must not fail"); + } + @Test void aDeregistrationAndACloseReleaseThePoolMBeanExactlyOnce() throws Exception { MBeanServer server = server(); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
