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 2625dc97f63fa6e73d9167fda3ce91744a854d62 Author: opencode <[email protected]> AuthorDate: Tue Nov 3 01:17:13 2026 +0100 Refuse to pool an XA session that carries an unresolved branch A pooled XA handle's close() recycles the session, but the pool's return-time transaction termination unwinds only local work and can never resolve an XA branch. Closing a spec-following handle between end/prepare and commit therefore re-pooled a session holding a live prepared branch and its database locks: the next borrower inherited them and the transaction manager's late commit landed on a session that does not carry the branch (XAER_NOTA / permanently in-doubt). The pool now wraps the driver's XA handle per session and books the branches started through it until they are committed, rolled back or forgotten. A returned session with an unresolved branch is closed and replaced rather than pooled - the resource manager drops the branch with the session - and late branch calls on a captured XAResource are refused with XAER_RMFAIL against the retired session instead of whatever the closed driver object happens to raise. Resolved branches still pool as before. Regression tests: TestUnresolvedXaBranchOnReturn. --- .../apache/tomcat/jdbc/pool/ConnectionPool.java | 14 + .../apache/tomcat/jdbc/pool/PooledConnection.java | 43 ++- .../apache/tomcat/jdbc/pool/ProxyConnection.java | 6 +- .../apache/tomcat/jdbc/pool/XABranchTracker.java | 302 +++++++++++++++++++++ .../org/apache/tomcat/jdbc/pool/XADataSource.java | 8 +- .../tomcat/jdbc/pool/util/LocalStrings.properties | 2 + .../jdbc/pool/TestUnresolvedXaBranchOnReturn.java | 171 ++++++++++++ 7 files changed, 539 insertions(+), 7 deletions(-) diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java index 87e5e33957..4c44a6f10d 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java @@ -1687,6 +1687,20 @@ public class ConnectionPool { if (isClosed()) { return true; } + if (con.hasUnresolvedXaBranches()) { + // A branch that was started through this session's XA resource + // and never committed, rolled back or forgotten stays attached to + // the physical connection, and neither the transaction + // termination below nor a plain rollback can resolve it: pooling + // the session would hand the branch and every database lock held + // for it to the next borrower, while the transaction manager's + // late commit would land on a session that does not carry it. + // Close the session instead - the resource manager drops the + // branch with it, and the manager's late call then fails visibly + // against a closed session. + log.warn(MessageFactory.get("connectionPool.unresolvedXaBranchReturned", con)); + return true; + } // Terminate the transaction before validating. When the pool is // configured to commit or roll back on return, that is the explicit // choice for the pending work, and a validation that runs first diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java index 8a7f27bf48..9161226119 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java @@ -79,7 +79,8 @@ public class PooledConnection implements PooledConnectionMBean { private volatile java.sql.Connection connection; /** - * If using a XAConnection underneath. + * If using a XAConnection underneath. On an XA session this is the pool's + * {@link XABranchTracker} wrapper around the handle the data source returned. */ protected volatile javax.sql.XAConnection xaConnection; /** @@ -382,6 +383,14 @@ public class PooledConnection implements PooledConnectionMBean { } throw failure; } + // Once the session is fully established, hand out the pool's + // wrapper rather than the driver's handle: it tracks the + // transaction branches started through this session, so a return + // can tell whether its borrower is leaving a branch behind (see + // hasUnresolvedXaBranches()). The connect above holds the driver's + // handle directly, and the wrapper below only delegates - a close + // through the field still physically closes the session. + xaConnection = new XABranchTracker(pooled); } else if (poolProperties.getDataSource() instanceof javax.sql.DataSource) { javax.sql.DataSource ds = (javax.sql.DataSource) poolProperties.getDataSource(); if (usr != null && pwd != null) { @@ -591,6 +600,14 @@ public class PooledConnection implements PooledConnectionMBean { // connection from being closed log.warn(MessageFactory.get("pooledConnection.interceptorDisconnectedFailed"), x); } + javax.sql.XAConnection retired = xaConnection; + if (retired instanceof XABranchTracker) { + // Mark the handle retired before the physical close: branch + // work on a resource captured from this handle then meets the + // pool's clean XA refusal instead of the driver's half-closed + // state. + ((XABranchTracker) retired).markClosed(); + } try { if (cpdsConnection != null) { // Closing the handle closes the physical connection @@ -1186,12 +1203,34 @@ public class PooledConnection implements PooledConnectionMBean { /** * Returns the underlying XA connection * - * @return the underlying XA connection as it was returned from the Datasource + * @return the XA connection of this session on an XA pool - the pool's + * {@link XABranchTracker} wrapper around the handle that the data + * source returned, which delegates every call to it - or null when + * the pool is not backed by an XA data source or this session has + * been disconnected */ public javax.sql.XAConnection getXAConnection() { return this.xaConnection; } + /** + * Reports whether this session carries an XA transaction branch that was + * started through its XA resource and never resolved. Such a branch is + * attached to the physical connection until the database drops it - the + * pool's return-time transaction termination unwinds only local work - so + * the session must not go back to the idle queue with one on it. + * + * @return true if at least one branch started on this session was never + * committed, rolled back or forgotten + */ + boolean hasUnresolvedXaBranches() { + javax.sql.XAConnection xa = xaConnection; + if (xa instanceof XABranchTracker) { + return ((XABranchTracker) xa).hasUnresolvedBranches(); + } + return false; + } + /** * Returns the timestamp of when the connection was last connected to the database. ie, a successful call to diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java index c8e7c6d8e7..f5c9859c28 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ProxyConnection.java @@ -379,8 +379,10 @@ public class ProxyConnection extends JdbcInterceptor { // connection behind it also provides - java.sql.Connection // first of all - keep resolving to the physical connection; // javax.sql.XAConnection is excluded because unwrapping to it - // has always yielded the driver's XA connection object where one - // exists, and the failure below where none does. This answer + // has always yielded the XA connection object the pooled + // connection holds - the driver's handle, wrapped by the pool's + // branch tracker on an XA session - and the failure below where + // none does. This answer // runs below the fence like every other handout here: above it, // a marking or a seal landing with the caller's re-check left // could hand the handle out on a borrow the pool was already diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XABranchTracker.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XABranchTracker.java new file mode 100644 index 0000000000..a8066525b8 --- /dev/null +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XABranchTracker.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.jdbc.pool; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.sql.ConnectionEventListener; +import javax.sql.StatementEventListener; +import javax.sql.XAConnection; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import org.apache.tomcat.jdbc.pool.util.MessageFactory; + +/** + * The pool's XA connection wrapper. It hands the driver's handle out to the + * pool and to borrowers unchanged in every call - it only delegates - and + * books, next to it, the transaction branches that were started through this + * session's XA resource and never resolved: committed, rolled back or + * forgotten. + * <p> + * The bookkeeping exists because a borrow's {@code close()} recycles the + * session instead of closing it, while the return-time transaction + * termination unwinds only local work and can never resolve an XA branch. A + * session that a borrower abandons mid-branch would otherwise go back into + * the idle queue carrying the branch and every database lock held for it, + * and the transaction manager's late commit would land on a session that + * does not carry it. The pool consults {@link #hasUnresolvedBranches()} when + * a connection is returned and retires such a session instead of pooling it. + */ +final class XABranchTracker implements XAConnection { + + /** + * The identity a branch is booked and resolved under. Xid implementations + * compare by value per the XA specification, but nothing forces a + * transaction manager's Xid to honour that, and the pool must not mistake + * a re-created equal branch for a new one: the key snapshots the content + * itself, and with it also survives a caller mutating its arrays. + */ + private static final class XidKey { + private final int formatId; + private final byte[] globalId; + private final byte[] branchId; + private final int hash; + + XidKey(Xid xid) { + formatId = xid.getFormatId(); + byte[] gtid = xid.getGlobalTransactionId(); + globalId = gtid == null ? new byte[0] : gtid.clone(); + byte[] bqual = xid.getBranchQualifier(); + branchId = bqual == null ? new byte[0] : bqual.clone(); + hash = Arrays.hashCode(globalId) * 31 + formatId; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof XidKey)) { + return false; + } + XidKey other = (XidKey) obj; + return formatId == other.formatId + && Arrays.equals(globalId, other.globalId) + && Arrays.equals(branchId, other.branchId); + } + } + + private final XAConnection delegate; + + /** + * Branches started on this session that the resource manager still + * considers attached to it. {@code end()} does not detach a branch - + * only commit, rollback or a successful forget does, and neither does a + * refusal of any of them: a branch whose resolution failed stays booked, + * because whether the resource manager kept it is then unknown and + * retiring the session is the only safe answer. + */ + private final Set<XidKey> unresolved = ConcurrentHashMap.newKeySet(); + + /** + * The one tracking resource this handle owns, so that repeated + * {@code getXAResource()} calls name the same object the way the driver's + * own handle does, and the bookkeeping cannot depend on which of the + * objects a transaction manager happened to keep. + */ + private volatile XAResource trackedResource; + + /** + * Set when the pool retires the session behind this handle. Branch + * lifecycle calls on a resource captured from the handle are then refused + * right here instead of reaching the closed driver object: a discarded + * session is terminal, its branches dropped with it, and a driver that + * does not fail cleanly on its own closed handle would otherwise surface + * the misuse as an unrelated runtime failure at the transaction manager. + */ + private volatile boolean closed; + + XABranchTracker(XAConnection delegate) { + this.delegate = delegate; + } + + /** + * Marks the session behind this handle as retired, from the pool's + * disconnect path. After this, no branch work is accepted any more. + */ + void markClosed() { + closed = true; + } + + /** + * Reports whether any branch started through this session is still + * unresolved. + * + * @return true if a branch was started and never committed, rolled back + * or forgotten on this session + */ + boolean hasUnresolvedBranches() { + return !unresolved.isEmpty(); + } + + // javax.sql.PooledConnection / javax.sql.XAConnection + + @Override + public Connection getConnection() throws SQLException { + return delegate.getConnection(); + } + + @Override + public void close() throws SQLException { + delegate.close(); + } + + @Override + public XAResource getXAResource() throws SQLException { + XAResource resource = trackedResource; + if (resource == null) { + synchronized (this) { + resource = trackedResource; + if (resource == null) { + resource = new TrackedResource(delegate.getXAResource()); + trackedResource = resource; + } + } + } + return resource; + } + + @Override + public void addConnectionEventListener(ConnectionEventListener listener) { + delegate.addConnectionEventListener(listener); + } + + @Override + public void removeConnectionEventListener(ConnectionEventListener listener) { + delegate.removeConnectionEventListener(listener); + } + + @Override + public void addStatementEventListener(StatementEventListener listener) { + delegate.addStatementEventListener(listener); + } + + @Override + public void removeStatementEventListener(StatementEventListener listener) { + delegate.removeStatementEventListener(listener); + } + + /** + * The tracked XA resource. Every call is dispatched to the driver's + * resource exactly as it is; the wrapper only books the branch lifecycle + * beside it. + */ + private final class TrackedResource implements XAResource { + + private final XAResource delegate; + + TrackedResource(XAResource delegate) { + this.delegate = delegate; + } + + /** + * Branch work on a retired session cannot succeed: the session is + * closed, its unresolved branches were dropped with it, and none of + * these calls reaches anything any more. The refusal names the + * resource manager unavailable, the code the XA specification gives + * for a resource that cannot serve the call, so the transaction + * manager sees a clean XA failure instead of whatever the closed + * driver object happens to raise. + */ + private void refuseRetiredSession() throws XAException { + if (closed) { + XAException failure = new XAException( + MessageFactory.get("xaBranchTracker.resourceRetired")); + failure.errorCode = XAException.XAER_RMFAIL; + throw failure; + } + } + + @Override + public void start(Xid xid, int flags) throws XAException { + refuseRetiredSession(); + delegate.start(xid, flags); + // Booked only once the resource manager accepted the branch: a + // refused start attached nothing to this session, so it leaves no + // debt. Joined and resumed branches land on the same key, so the + // set counts branches, not calls. + unresolved.add(new XidKey(xid)); + } + + @Override + public void end(Xid xid, int flags) throws XAException { + refuseRetiredSession(); + delegate.end(xid, flags); + } + + @Override + public int prepare(Xid xid) throws XAException { + refuseRetiredSession(); + int outcome = delegate.prepare(xid); + if (outcome == XAResource.XA_RDONLY) { + // The resource manager resolved the branch itself: a + // read-only branch is complete at prepare and no commit will + // ever come for it. + unresolved.remove(new XidKey(xid)); + } + return outcome; + } + + @Override + public void commit(Xid xid, boolean onePhase) throws XAException { + refuseRetiredSession(); + delegate.commit(xid, onePhase); + unresolved.remove(new XidKey(xid)); + } + + @Override + public void rollback(Xid xid) throws XAException { + refuseRetiredSession(); + delegate.rollback(xid); + unresolved.remove(new XidKey(xid)); + } + + @Override + public void forget(Xid xid) throws XAException { + refuseRetiredSession(); + delegate.forget(xid); + unresolved.remove(new XidKey(xid)); + } + + @Override + public Xid[] recover(int flag) throws XAException { + return delegate.recover(flag); + } + + @Override + public boolean setTransactionTimeout(int seconds) throws XAException { + return delegate.setTransactionTimeout(seconds); + } + + @Override + public int getTransactionTimeout() throws XAException { + return delegate.getTransactionTimeout(); + } + + @Override + public boolean isSameRM(XAResource xares) throws XAException { + // Two tracked resources are of the same resource manager exactly + // when the resources underneath them are; an unwrapped resource + // is compared against the driver's own object directly. + if (xares instanceof TrackedResource) { + return delegate.isSameRM(((TrackedResource) xares).delegate); + } + return delegate.isSameRM(xares); + } + } +} diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java index 7d20bd88e7..97e3b21e8c 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/XADataSource.java @@ -66,9 +66,11 @@ public class XADataSource extends DataSource implements javax.sql.XADataSource { * returns the session to the pool instead of closing the physical connection; every XA branch started through the * returned {@link javax.transaction.xa.XAResource} must therefore be completed - committed or rolled back - before * {@code close()}, and neither the handle nor its {@code XAResource} may be used after it. A handle closed while a - * branch stands prepared parks that in-doubt branch, and the database locks held for it, on a session the pool - * hands to another borrower: the pool's return-time rollback unwinds only local work and can never resolve a - * prepared branch. + * branch stands unresolved is not returned to the pool: the pool's return-time rollback unwinds only local work + * and can never resolve a branch that stands prepared, so the pool closes the session instead of handing another + * borrower a connection that carries that branch and the database locks held for it. The transaction manager's + * late commit or rollback of the abandoned branch then fails against the closed session rather than landing, + * silently, on a session that does not carry it. * <p> * The connection obtained from the returned handle's * {@link javax.sql.PooledConnection#getConnection()} is the driver's raw logical session, not a pool-managed diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties index f3cb9a5c2f..a2b0ecd3bd 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties @@ -69,6 +69,7 @@ connectionPool.unableToSendRequestHint=Unable to send the request boundary hint connectionPool.unableToStartJmx=Unable to start JMX integration for connection pool. Instance[{0}] can''t be monitored. connectionPool.unableToTestIdleConnection=Unable to test idle connection [{0}], closing it. connectionPool.unableToTerminateTransaction=Unable to terminate transaction, connection will be closed. +connectionPool.unresolvedXaBranchReturned=Connection [{0}] was returned with XA transaction branches that were never resolved. The session is closed and replaced instead of being pooled, so no other borrower inherits a branch or the database locks held for it, and the late commit or rollback of the abandoned branch fails against the closed session. Complete every branch - commit, rollback or forget - before closing a pooled XA handle. connectionPool.validationQueryFailed=Validation Query Failed, enable logValidationErrors for more details. connectionPool.asyncBorrowConnectionReleased=The pooled connection served to this asynchronous borrow was released before it could be set up; no connection was borrowed, borrow the pool again. @@ -212,6 +213,7 @@ statementFinalizer.unableToCreateTrackingProxy=Unable to create statement tracki trapException.uncaughtUnderlyingException=Uncaught underlying exception. +xaBranchTracker.resourceRetired=The pooled XA session behind this XA resource has been closed and taken out of service by the pool; the transaction branches left on it were dropped with the session and cannot be resolved any more. xaDataSource.dataSourceNotXA=The connection pool [{0}] cannot provide XA connections: the data source configured through the ''dataSource'' or ''dataSourceJNDI'' attribute ({1}) does not implement javax.sql.XADataSource. xaDataSource.jndiNotResolved=The connection pool [{0}] cannot provide XA connections: the name "{1}" set through the ''dataSourceJNDI'' attribute has not been resolved into a data source; the name is only looked up when the pool is created through the DataSourceFactory. Set the ''dataSource'' attribute to a javax.sql.XADataSource provided by the JDBC driver. xaDataSource.noDataSource=The connection pool [{0}] cannot provide XA connections: neither the ''dataSource'' nor the ''dataSourceJNDI'' attribute is set, so only plain JDBC connections from a JDBC driver and URL can be pooled. Set the ''dataSource'' or ''dataSourceJNDI'' attribute to a javax.sql.XADataSource provided by the JDBC driver. diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestUnresolvedXaBranchOnReturn.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestUnresolvedXaBranchOnReturn.java new file mode 100644 index 0000000000..9145cd8b7f --- /dev/null +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestUnresolvedXaBranchOnReturn.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.jdbc.pool; + +import java.sql.Connection; +import java.sql.Statement; + +import javax.sql.XAConnection; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.apache.tomcat.jdbc.JdbcPoolBaseTest; + +/** + * A returned handle is a pool borrow, not a physical close, and the pool's + * return-time transaction termination unwinds only local work: it can never + * resolve an XA branch. A handle closed between {@code xa.end} and + * {@code xa.commit} therefore used to re-pool the session with the branch - + * and every database lock held for it - attached, so the next borrower + * inherited them and the transaction manager's late commit landed on a + * session that does not carry the branch. The pool must instead take such a + * session out of service, so the resource manager drops the branch with the + * session and the late commit fails visibly instead of silently misfiring + * across borrowers. + */ +class TestUnresolvedXaBranchOnReturn extends JdbcPoolBaseTest { + + /** 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 aPreparedBranchOutlivingTheHandleKeepsTheSessionOutOfThePool() throws Exception { + XADataSource ds = newXaPool(); + String t = uniqueTable(); + // The table itself is committed work, so the branch's row stays + // countable after the branch - and with it the session - is retired. + try (Connection setup = ds.getConnection(); + Statement st = setup.createStatement()) { + st.execute("CREATE TABLE " + t + " (v INT)"); + } + + XAConnection xc = ds.getXAConnection(); + XAResource xa = xc.getXAResource(); + Assertions.assertSame(xa, xc.getXAResource(), + "repeated calls must name the same XA resource"); + Connection c = xc.getConnection(); + c.setAutoCommit(false); + Xid xid = new TestXid(11); + + xa.start(xid, XAResource.TMNOFLAGS); + try (Statement st = c.createStatement()) { + st.execute("INSERT INTO " + t + " VALUES (1)"); + } + setSessionMarker(c, 77); + xa.end(xid, XAResource.TMSUCCESS); + Assertions.assertEquals(XAResource.XA_OK, xa.prepare(xid)); + Assertions.assertEquals(0, adminRowCount(t), "work must be invisible before commit"); + + // The spec-shaped misuse: abandon the handle between end/prepare and + // commit. A physical close would have dropped the branch; the pool + // must not turn that close into a re-pool. + xc.close(); + + Assertions.assertEquals(0, poolSessions(), + "a session carrying an unresolved prepared branch must not stay in the pool"); + + // The late commit of an abandoned branch must fail visibly against + // the retired session, not quietly succeed on a re-pooled one - and + // as a refusal of the resource manager, the code the XA + // specification gives for a resource that can no longer serve. + XAException refused = Assertions.assertThrows(XAException.class, () -> xa.commit(xid, false), + "committing a branch whose session the pool retired must fail"); + Assertions.assertEquals(XAException.XAER_RMFAIL, refused.errorCode); + Assertions.assertEquals(0, adminRowCount(t), + "the abandoned branch must not resurface through another borrower"); + + try (Connection again = ds.getConnection()) { + Assertions.assertNull(sessionMarker(again), + "the next borrower must not inherit the session the branch was parked on"); + } + } + + @Test + void anOpenBranchOutlivingTheHandleKeepsTheSessionOutOfThePool() throws Exception { + XADataSource ds = newXaPool(); + String t = uniqueTable(); + try (Connection setup = ds.getConnection(); + Statement st = setup.createStatement()) { + st.execute("CREATE TABLE " + t + " (v INT)"); + } + + XAConnection xc = ds.getXAConnection(); + XAResource xa = xc.getXAResource(); + Connection c = xc.getConnection(); + c.setAutoCommit(false); + Xid xid = new TestXid(12); + + // A branch that stands started and never resolved is just as attached + // to the session as a prepared one: end() alone would not detach it + // either, and the locks its work took are held the same way. + xa.start(xid, XAResource.TMNOFLAGS); + try (Statement st = c.createStatement()) { + st.execute("INSERT INTO " + t + " VALUES (1)"); + } + setSessionMarker(c, 78); + + xc.close(); + + Assertions.assertEquals(0, poolSessions(), + "a session carrying an unresolved branch must not stay in the pool"); + XAException refused = Assertions.assertThrows(XAException.class, () -> xa.rollback(xid), + "resolving a branch whose session the pool retired must fail"); + Assertions.assertEquals(XAException.XAER_RMFAIL, refused.errorCode); + Assertions.assertEquals(0, adminRowCount(t), + "the abandoned branch's work must not resurface through another borrower"); + + try (Connection again = ds.getConnection()) { + Assertions.assertNull(sessionMarker(again), + "the next borrower must not inherit the session the branch was parked on"); + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
