synchs; // the Synchronizations in the group
+ volatile ExecutionStatus status; // track the status to decide when it's too late to allow more registrations
+
+ public SynchronizationGroup(String packagePrefix) {
+ this.packagePrefix = packagePrefix;
+ this.synchs = new ArrayList<>();
+ this.status = ExecutionStatus.PENDING;
+ }
+
+ public void add(Synchronization synchronization) {
+ if (status == ExecutionStatus.FINISHED) {
+ // this group of syncs have already ran
+ throw new IllegalStateException(ADD_SYNC_ERROR);
+ }
+ synchs.add(synchronization);
+ }
+
+ @Override
+ public void beforeCompletion() {
+ status = ExecutionStatus.RUNNING;
+
+ // Note that because synchronizations can register other synchronizations
+ // we cannot use enhanced for loops as that could cause a concurrency exception
+ for (int i = 0; i < synchs.size(); i++) {
+ Synchronization sync = synchs.get(i);
+
+ try {
+ sync.beforeCompletion();
+ } catch (Exception e) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf(
+ "The synchronization %s associated with tx key %s failed during beforeCompletion: %s",
+ sync, tsr.getTransactionKey(), e.getMessage());
+ }
+
+ if (deferredThrowable == null) {
+ // only save the first failure
+ deferredThrowable = e;
+ }
+ }
+ }
+
+ status = ExecutionStatus.FINISHED;
+ }
+
+ @Override
+ public void afterCompletion(int status) {
+ // The list should be iterated in reverse order
+ for (int i = synchs.size(); i-- > 0;) {
+ synchs.get(i).afterCompletion(status);
+ }
+ }
+
+ // does packageName belong to this group of synchronizations
+ private boolean shouldAdd(String packageName) {
+ return !packagePrefix.isEmpty() && packageName.startsWith(packagePrefix);
+ }
+ }
+
+ public AgroalOrderedLastSynchronizationList(
+ TransactionSynchronizationRegistryWrapper transactionSynchronizationRegistryWrapper) {
+
+ this.tsr = transactionSynchronizationRegistryWrapper;
+
+ for (var packagePrefix : PKG_PREFIXES) {
+ var synchronizationGroup = new SynchronizationGroup(packagePrefix);
+
+ synchGroups.add(synchronizationGroup);
+
+ if (packagePrefix.isEmpty()) {
+ otherSynchs = synchronizationGroup; // the catch-all group
+ }
+ }
+ }
+
+ /**
+ * Register an interposed synchronization. Note that synchronizations are not allowed if:
+ *
+ *
+ * @param synchronization The synchronization to register
+ * @throws IllegalStateException if the transaction is in the wrong state:
+ *
+ * - the transaction has already prepared;
+ *
- the transaction is marked rollback only
+ *
- the group that the synchronization should belong to has already been processed
+ *
+ */
+ public void registerInterposedSynchronization(Synchronization synchronization) {
+ int status = tsr.getTransactionStatus();
+
+ switch (status) {
+ case Status.STATUS_ACTIVE:
+ case Status.STATUS_PREPARING:
+ break;
+ default:
+ throw new IllegalStateException(REGISTER_SYNC_ERROR + status);
+ }
+
+ // add the synchronization to the group that matches this package and, if there is no such group
+ // then add it to the catch-all group (otherSyncs)
+ String packageName = synchronization.getClass().getName();
+ SynchronizationGroup synchGroup = otherSynchs;
+
+ for (SynchronizationGroup g : synchGroups) {
+ if (g.shouldAdd(packageName)) {
+ synchGroup = g;
+ break;
+ }
+ }
+
+ synchGroup.add(synchronization);
+ }
+
+ /**
+ * Exceptions from beforeCompletion Synchronizations are not caught because such errors should cause the
+ * transaction to roll back.
+ */
+ @Override
+ public void beforeCompletion() {
+ // run each group of synchs according to the order they were added to the list
+ for (SynchronizationGroup g : synchGroups) {
+ g.beforeCompletion();
+ }
+
+ if (deferredThrowable != null) {
+ /*
+ * If any Synchronization threw an exception then only report the first one.
+ *
+ * Cause the transaction to rollback. The underlying transaction manager will catch the runtime
+ * exception and re-throw it when it does the rollback
+ */
+ throw new RuntimeException(deferredThrowable);
+ }
+ }
+
+ @Override
+ public void afterCompletion(int status) {
+ // run each group of synchs according to the order they were added to the list
+ for (SynchronizationGroup g : synchGroups) {
+ g.afterCompletion(status);
+ }
+ }
+}
diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/TransactionSynchronizationRegistryWrapper.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/TransactionSynchronizationRegistryWrapper.java
new file mode 100644
index 0000000000000..3c88840159e7d
--- /dev/null
+++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/internal/tsr/TransactionSynchronizationRegistryWrapper.java
@@ -0,0 +1,87 @@
+package io.quarkus.narayana.jta.runtime.internal.tsr;
+
+import jakarta.transaction.Synchronization;
+import jakarta.transaction.TransactionSynchronizationRegistry;
+
+import org.jboss.logging.Logger;
+
+import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple;
+
+/**
+ * Agroal registers an interposed synchronization which validates that connections have been released.
+ * Components such as hibernate release connections in an interposed synchronization.
+ * Therefore, we must ensure that Agroal runs last.
+ *
+ *
+ * This wrapper re-orders interposed synchronizations as follows: [other, hibernate-orm, agroal].
+ *
+ *
+ * Synchronizations are placed into groups according to their package name and the groups are ordered which means
+ * that all hibernate synchronizations run before Agroal ones and all other synchs run before the hibernate ones.
+ *
+ *
+ * See {@code AgroalOrderedLastSynchronizationList} for details of the re-ordering.
+ */
+public class TransactionSynchronizationRegistryWrapper implements TransactionSynchronizationRegistry {
+ private final Object key = new Object();
+ private static final Logger LOG = Logger.getLogger(TransactionSynchronizationRegistryWrapper.class);
+
+ private final TransactionSynchronizationRegistryImple tsr;
+ private transient com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple delegate;
+
+ public TransactionSynchronizationRegistryWrapper(
+ TransactionSynchronizationRegistryImple transactionSynchronizationRegistryImple) {
+ this.tsr = transactionSynchronizationRegistryImple;
+ }
+
+ @Override
+ public void registerInterposedSynchronization(Synchronization sync) {
+ AgroalOrderedLastSynchronizationList agroalOrderedLastSynchronization = (AgroalOrderedLastSynchronizationList) tsr
+ .getResource(key);
+
+ if (agroalOrderedLastSynchronization == null) {
+ synchronized (key) {
+ agroalOrderedLastSynchronization = (AgroalOrderedLastSynchronizationList) tsr.getResource(key);
+ if (agroalOrderedLastSynchronization == null) {
+ agroalOrderedLastSynchronization = new AgroalOrderedLastSynchronizationList(this);
+
+ tsr.putResource(key, agroalOrderedLastSynchronization);
+ tsr.registerInterposedSynchronization(agroalOrderedLastSynchronization);
+ }
+ }
+ }
+
+ // add the synchronization to the list that does the reordering
+ agroalOrderedLastSynchronization.registerInterposedSynchronization(sync);
+ }
+
+ @Override
+ public Object getTransactionKey() {
+ return tsr.getTransactionKey();
+ }
+
+ @Override
+ public int getTransactionStatus() {
+ return tsr.getTransactionStatus();
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return tsr.getRollbackOnly();
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ tsr.setRollbackOnly();
+ }
+
+ @Override
+ public Object getResource(Object key) {
+ return tsr.getResource(key);
+ }
+
+ @Override
+ public void putResource(Object key, Object value) {
+ tsr.putResource(key, value);
+ }
+}