{"record":{"id":"18410ba32d49d207","repo":"quarkusio/quarkus","slug":"transaction-was-already-rolled-back-e-g-by-the","errorCode":null,"errorMessage":"Transaction was already rolled back (e.g., by the transaction reaper)","messagePattern":"Transaction was already rolled back \\(e\\.g\\., by the transaction reaper\\)","errorType":"exception","errorClass":"RollbackException","httpStatus":null,"severity":"error","filePath":"extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java","lineNumber":453,"sourceCode":"                tx.setRollbackOnly();\n            }\n        } catch (IllegalStateException e) {\n            log.warn(\"Cannot set rollback-only, transaction already ended\", e);\n        }\n    }\n\n    protected void handleException(InvocationContext ic, Throwable t, Transaction tx) throws Exception {\n\n        handleExceptionNoThrow(ic, t, tx);\n        sneakyThrow(t);\n    }\n\n    protected void endTransaction(TransactionManager tm, Transaction tx, RunnableWithException afterEndTransaction)\n            throws Exception {\n        try {\n            Transaction current = tm.getTransaction();\n            if (current == null) {\n                throw new RollbackException(\"Transaction was already rolled back (e.g., by the transaction reaper)\");\n            }\n            if (tx != current) {\n                throw new RuntimeException(jtaLogger.i18NLogger.get_wrong_tx_on_thread());\n            }\n\n            if (tx.getStatus() == Status.STATUS_MARKED_ROLLBACK) {\n                tm.rollback();\n            } else {\n                tm.commit();\n            }\n        } finally {\n            afterEndTransaction.run();\n        }\n    }\n\n    protected boolean setUserTransactionAvailable(boolean available) {\n        boolean previousUserTransactionAvailability = ServerVMClientUserTransaction.isAvailable();\n","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java#L435-L471","documentation":"At the end of a @Transactional method, endTransaction commits/rolls back the transaction, but first it verifies a transaction still exists on the thread. If tm.getTransaction() returns null, the transaction vanished — typically because the Narayana transaction reaper aborted it after the timeout — and this RollbackException is thrown. It signals your transaction exceeded its timeout and work was rolled back behind your back.","triggerScenarios":"A @Transactional method runs longer than the configured transaction timeout (default 60s); the reaper rolls back and aborts the TX; when the interceptor later calls endTransaction, the thread's transaction is gone, so tm.getTransaction()==null. Also invoked from handleAsync for deferred completion.","commonSituations":"Slow external calls (HTTP, DB queries) inside a transaction exceeding the timeout; large batch processing in one TX; deadlock/stuck DB connection until timeout; default timeout too small for the workload.","solutions":["Increase the transaction timeout with @TransactionConfiguration(timeout=...) on the entry method or quarkus.transaction.default-transaction-timeout globally","Move slow external I/O outside the transaction boundary so the TX window stays short","Tune the reaper (quarkus.transaction.periodic-recovery-period / reaper-related settings) and investigate why the TX ran so long","Handle work loss idempotently — the TX rolled back, so retry the operation once the slowness is fixed"],"exampleFix":"// before\n@Transactional\nvoid importAll() { slowHttpCall(); ... } // >60s, reaper aborts\n// after\n@Transactional @TransactionConfiguration(timeout = 600)\nvoid importAll() { ... }","handlingStrategy":"retry","validationCode":"int status = transactionManager.getStatus();\nboolean txStillAlive = (status == jakarta.transaction.Status.STATUS_ACTIVE);\n// if not alive before committing, the reaper already killed it","typeGuard":null,"tryCatchPattern":"try { @Transactional work } catch (RollbackException e) {\n    if (e.getMessage().contains(\"already rolled back\")) { /* retry idempotently with larger timeout */ }\n}","preventionTips":["Keep transactions shorter than the configured timeout","Move slow HTTP/file I/O outside transaction boundaries","Raise timeout via @TransactionConfiguration(timeout=) on the entry method","Monitor for reaper warnings (ARJUNA012117 etc.) in logs"],"tags":["jta","transaction","timeout","rollback","reaper","narayana"],"backgroundTag":"transaction-timed-out-and-rolled-back","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}