Skip to content

JCR-5239: Skip direct JDBC tx-control on managed XA connection during active JTA transaction#350

Open
torsten-liermann wants to merge 1 commit intoapache:trunkfrom
torsten-liermann:feature/jta-aware-connection-helper
Open

JCR-5239: Skip direct JDBC tx-control on managed XA connection during active JTA transaction#350
torsten-liermann wants to merge 1 commit intoapache:trunkfrom
torsten-liermann:feature/jta-aware-connection-helper

Conversation

@torsten-liermann
Copy link
Copy Markdown

@torsten-liermann torsten-liermann commented Apr 30, 2026

Issue

JIRA: JCR-5239

ConnectionHelper in jackrabbit-data issues three direct JDBC transaction-control calls (setAutoCommit(false) in startBatch(), commit()/rollback() in endBatch(), setAutoCommit(true) in getConnection() from JCR-1013). On a JCA-managed connection pool inside an active container-managed JTA transaction these calls are rejected by the JCA contract — IronJacamar reports IJ031017 / IJ031018 / IJ031019 because the transaction manager owns the connection's transactional lifecycle. This blocks every Jackrabbit-Classic deployment that wants to share its DataSource with a Jakarta-EE JTA boundary (e.g. JPA + JCR 2PC on the same backend).

Fix

A nested static JtaContext discovers the ambient TransactionSynchronizationRegistry lazily on first use via JNDI (java:comp/TransactionSynchronizationRegistry) and exposes two operations to ConnectionHelper:

  • hasManagedTransaction() — returns true for every JTA Status other than STATUS_NO_TRANSACTION. STATUS_UNKNOWN is treated as managed because per the JTA contract it means a transaction is associated but its current status cannot be determined; running direct JDBC tx-control on a possibly enrolled connection is the very failure mode this PR addresses. Fail-closed in three layers: TSR present + status method unavailable -> true; reflective invocation failure -> true; non-integer return -> true.
  • markRollbackOnly() — invoked from endBatch(false) when a managed JTA tx is in progress. The local Connection.rollback() cannot run on a JCA-managed XA connection, so the caller's rollback intent is mirrored onto the global JTA transaction via the TSR; the TM aborts the global transaction on completion. Returns boolean; endBatch(false) throws SQLException when false is returned, so a requested rollback never disappears silently.

Method binding tries jakarta.transaction.TransactionSynchronizationRegistry first, then javax.transaction.TransactionSynchronizationRegistry, then falls back to the concrete TSR class — so the same compiled class works against javax.transaction (Java EE 8) and jakarta.transaction (Jakarta EE 9+) without conditional code paths and without depending on a package-private container implementation type.

When no TSR is bound (standalone JSE / unit-test setups), the historical behaviour is fully preserved: setAutoCommit/commit/rollback run directly on the JDBC Connection exactly as before.

Mid-batch JTA-state changes

The JTA state is sampled per call site (startBatch, endBatch, getConnection), not captured once at startBatch and replayed at endBatch. The reasoning:

  • In a real Jackrabbit deployment a single startBatch/endBatch pair is executed within one persistence-manager operation, which runs entirely inside whichever JTA scope the caller established. The state the helper sees at startBatch is the same state it sees at endBatch.
  • If the JTA scope nevertheless changes between the two call sites, the helper makes the locally correct choice at each point: under managed JTA it skips the JDBC tx-control calls that the pool would reject; outside managed JTA it issues them as before. On a JCA-managed pool any stray auto-commit flag that survives a batch boundary is normalised when the connection returns to the pool, so cross-batch state leakage is bounded by the pool's own contract.
  • The asymmetric case startBatch outside managed JTA + endBatch inside managed JTA: startBatch already issued setAutoCommit(false) on the local Connection. endBatch under managed JTA must not call commit()/rollback() (the JCA pool would reject them); for commit==false the rollback intent is still mirrored onto the global transaction via setRollbackOnly(). The local Connection's autoCommit flag is left untouched and the pool resets it on connection return.

Diff scope

  • jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ConnectionHelper.java — JTA detection logic in a new package-private JtaContext static nested class, lazy lookup, three call-site adjustments in startBatch(), endBatch() and getConnection(boolean).
  • jackrabbit-data/src/test/java/org/apache/jackrabbit/core/util/db/ConnectionHelperJtaContextTest.java — JUnit unit tests around the JtaContext decision logic: no TSR / STATUS_NO_TRANSACTION / STATUS_ACTIVE / STATUS_MARKED_ROLLBACK / STATUS_UNKNOWN / TSR present but getTransactionStatus() throws / TSR present but getTransactionStatus method missing / markRollbackOnly() delegation, failure, absent-TSR, missing-method cases.
  • jackrabbit-data/src/test/java/org/apache/jackrabbit/core/util/db/ConnectionHelperJtaIT.java — tests against ConnectionHelper itself with a hand-rolled DataSource/Connection stand-in: startBatch() skips/calls setAutoCommit(false) based on JTA state; endBatch(true) skips commit() under managed JTA; endBatch(false) calls setRollbackOnly() under managed JTA, throws SQLException if that fails, calls Connection.rollback() outside; getConnection(false) skips/calls the JCR-1013 auto-commit flip based on JTA state.

No behavioural change outside an active managed JTA transaction.

Verification

  • 18 unit tests in this PR cover the decision matrix and the three call sites without bringing up a JNDI context (11 in ConnectionHelperJtaContextTest, 7 in ConnectionHelperJtaIT).
  • End-to-end validation in a separate demo project: WildFly 35.0.1.Final (IronJacamar) with <xa-data-source> SQL Server 2022 via SQLServerXADataSource, Spring Boot 3.5.14 WAR using stock jackrabbit-core 2.22.2 with this fix overlaid on jackrabbit-data. 2PC scenarios across JPA + JCR-Session XAResources within one Narayana JTA transaction (4 rollback modes + 2 DataStore-rollback modes), all green; com.arjuna.ats.jta TRACE log shows multiple enlistResource / prepare / commit-or-abort calls per Tx with matching tx_uid.

@torsten-liermann torsten-liermann force-pushed the feature/jta-aware-connection-helper branch 3 times, most recently from 8a8a272 to 165f8b6 Compare April 30, 2026 15:16
… active JTA transaction

When ConnectionHelper runs inside a Jakarta/Java EE container (WildFly,
JBoss, ...) the DataSource is typically a JCA-managed connection pool.
While the application thread holds an active container-managed JTA
transaction, the wrapped Connection refuses explicit setAutoCommit(false),
commit() and rollback() calls (e.g. IronJacamar IJ031017 / IJ031018 /
IJ031019) because the transaction manager owns the connection's
transactional lifecycle.

This change resolves a TransactionSynchronizationRegistry once via JNDI
(java:comp/TransactionSynchronizationRegistry) and uses its
getTransactionStatus() to detect a managed transaction on the current
thread. When such a transaction is active, the three direct JDBC
transaction-control calls in startBatch(), endBatch() and getConnection()
are skipped — the TM commits/rolls back the underlying XAResource as part
of the global transaction.

When no managed transaction is in progress, the historical behaviour is
preserved (auto-commit toggling and explicit commit/rollback as before).

The TSR is referenced through reflection so the same code compiles and
runs against javax.transaction (Java EE 8) and jakarta.transaction
(Jakarta EE 9+) container stacks.

Signed-off-by: Torsten Liermann <mist@liermann.biz>
@torsten-liermann torsten-liermann force-pushed the feature/jta-aware-connection-helper branch from 165f8b6 to 21ec132 Compare April 30, 2026 15:34
@torsten-liermann torsten-liermann marked this pull request as ready for review April 30, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant