From 92381881648f16d0784d84a3ce0da4430d984913 Mon Sep 17 00:00:00 2001 From: Bassem Hamdi Date: Sun, 26 Jul 2026 00:21:28 +0200 Subject: [PATCH] Reset the rollbackOnly marker when a managed connection is recycled LocalAndXATransaction.end(xid, TMFAIL) marks the TransactionContext rollbackOnly so that no further work is done in the transaction that has just failed (AMQ-7485). The marker is only ever cleared by TransactionContext.begin() or start(xid, flags), that is, at the next transaction boundary on that context. In the resource adapter the TransactionContext is owned by ActiveMQManagedConnection, so it lives as long as the pooled physical connection rather than as long as the transaction. When the application server returns such a connection to the pool after a rolled back transaction and later hands it out again for work that is not part of a transaction, there is no new transaction boundary, the marker is still set, and ActiveMQSession.send() rejects every send with "transaction marked rollback only". The connection stays unusable for non transacted sends until it happens to be enlisted in a transaction again. Clear the marker in LocalAndXATransaction.cleanup(). That method is only reached from ManagedConnection.cleanup(), which is the point at which the container recycles the physical connection, and it already discards the other transaction scoped state via TransactionContext.cleanup(). end(TMFAIL) still leaves the marker set for the session that took part in the failed transaction, so the behaviour asserted by AMQ-7485 is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../activemq/ra/LocalAndXATransaction.java | 7 ++++ .../ra/JmsXAQueueTransactionTest.java | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/activemq-ra/src/main/java/org/apache/activemq/ra/LocalAndXATransaction.java b/activemq-ra/src/main/java/org/apache/activemq/ra/LocalAndXATransaction.java index 339715427ee..1e3249c6d8a 100644 --- a/activemq-ra/src/main/java/org/apache/activemq/ra/LocalAndXATransaction.java +++ b/activemq-ra/src/main/java/org/apache/activemq/ra/LocalAndXATransaction.java @@ -167,6 +167,13 @@ public boolean isInManagedTx() { public void cleanup() { transactionContext.cleanup(); + // The managed connection is being recycled, so transaction scoped state must not + // survive into its next use. The rollbackOnly marker set by end(TMFAIL) belongs to + // the transaction that has just ended; left in place it is only ever cleared by the + // next transaction boundary on this context, so a physical connection returned to + // the pool after a rolled back transaction rejects every subsequent send that is + // not part of a transaction. + transactionContext.setRollbackOnly(false); inManagedTx = false; } diff --git a/activemq-ra/src/test/java/org/apache/activemq/ra/JmsXAQueueTransactionTest.java b/activemq-ra/src/test/java/org/apache/activemq/ra/JmsXAQueueTransactionTest.java index f5897df5498..6ef08809cdf 100644 --- a/activemq-ra/src/test/java/org/apache/activemq/ra/JmsXAQueueTransactionTest.java +++ b/activemq-ra/src/test/java/org/apache/activemq/ra/JmsXAQueueTransactionTest.java @@ -174,6 +174,48 @@ public void testSendOnAbortedXATx() throws Exception { connection.close(); } + // The rollbackOnly marker is scoped to the transaction that failed, but the + // TransactionContext it lives on is owned by the physical connection. Once the managed + // connection has been recycled it must be usable again, including for work that is not + // part of any transaction, otherwise a single rolled back transaction disables the + // pooled connection for every later non transacted send. + public void testSendOnRecycledManagedConnectionAfterAbortedXATx() throws Exception { + connection.close(); + + ConnectionFactory connectionFactory = newConnectionFactory(); + connection = connectionFactory.createConnection(); + connection.start(); + + ManagedConnectionProxy proxy = (ManagedConnectionProxy) connection; + ActiveMQManagedConnection mc = (ActiveMQManagedConnection) proxy.getManagedConnection(); + xaResource = mc.getXAResource(); + + beginTx(); + + Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); + session.createProducer(destination); + + abortTx(); + + assertTrue("aborted tx leaves the context rollbackOnly", mc.getTransactionContext().isRollbackOnly()); + + // the container returns the managed connection to the pool + mc.cleanup(); + + assertFalse("recycled managed connection must not stay rollbackOnly", + mc.getTransactionContext().isRollbackOnly()); + + // and hands it out again, this time outside of any transaction + ManagedConnectionProxy recycled = (ManagedConnectionProxy) mc.getConnection(null, null); + recycled.start(); + Session recycledSession = recycled.createSession(false, Session.AUTO_ACKNOWLEDGE); + MessageProducer recycledProducer = recycledSession.createProducer(destination); + + recycledProducer.send(recycledSession.createTextMessage("sent after the managed connection was recycled")); + + recycled.close(); + } + public void testReceiveTwoThenAbort() throws Exception { Message[] outbound = new Message[] {session.createTextMessage("First Message"), session.createTextMessage("Second Message")};