{"record":{"id":"40d0e5862da739d8","repo":"apache/pulsar","slug":"transaction-txnid-cannot-transaction-from-sta","errorCode":null,"errorMessage":"Transaction `${txnID}` CANNOT transaction from status ${txnStatus} to ${newStatus}","messagePattern":"Transaction `(.+?)` CANNOT transaction from status (.+?) to (.+?)","errorType":"exception","errorClass":"InvalidTxnStatusException","httpStatus":null,"severity":"error","filePath":"pulsar-transaction/coordinator/src/main/java/org/apache/pulsar/transaction/coordinator/impl/TxnMetaImpl.java","lineNumber":149,"sourceCode":"        return this;\n    }\n\n    /**\n     * Update the transaction stats from the <tt>newStatus</tt> only when\n     * the current status is the expected <tt>expectedStatus</tt>.\n     *\n     * @param newStatus the new transaction status\n     * @param expectedStatus the expected transaction status\n     * @return the transaction itself.\n     * @throws InvalidTxnStatusException\n     */\n    @Override\n    public synchronized TxnMetaImpl updateTxnStatus(TxnStatus newStatus,\n                                                    TxnStatus expectedStatus)\n        throws InvalidTxnStatusException {\n        checkTxnStatus(expectedStatus);\n        if (!TransactionUtil.canTransitionTo(txnStatus, newStatus)) {\n            throw new InvalidTxnStatusException(\n                \"Transaction `\" + txnID + \"` CANNOT transaction from status \" + txnStatus + \" to \" + newStatus);\n        }\n        this.txnStatus = newStatus;\n        return this;\n    }\n\n    @Override\n    public long getOpenTimestamp() {\n        return this.openTimestamp;\n    }\n\n    @Override\n    public long getTimeoutAt() {\n        return this.timeoutAt;\n    }\n\n    @Override\n    public String getOwner() {","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-transaction/coordinator/src/main/java/org/apache/pulsar/transaction/coordinator/impl/TxnMetaImpl.java#L131-L167","documentation":"TxnMetaImpl.updateTxnStatus enforces the transaction state machine: it first checks the expected current status, then verifies the requested new status is a legal transition via TransactionUtil.canTransitionTo. If the transaction coordinator/owner attempts a transition the state machine forbids (e.g. OPEN -> ABORTING is fine, but ABORTED -> COMMITTING is not), it throws InvalidTxnStatusException.","triggerScenarios":"Calling updateTxnStatus(newStatus, expectedStatus) where TransactionUtil.canTransitionTo(currentTxnStatus, newStatus) is false — e.g. double-committing a transaction, committing an already-aborted txn, or racing two coordinators/endpoints to move the same txn.","commonSituations":"Client retries commit/abort after the transaction already reached terminal state; timeout handler aborts a txn the client is concurrently committing; stale coordinator state after failover; application bug performing operations past a terminal status.","solutions":["Check the transaction's current status (via coordinator admin API) before requesting a transition and only issue legal transitions","Treat the transaction as finished if it is already in COMMITTED/ABORTED — do not retry commit/abort against terminal states","Guard against client/timeout races: rely on the expectedStatus parameter so concurrent transitions fail fast and the loser backs off","Keep client and broker versions consistent; older/newer TxnStatus handling can produce transitions the other side considers illegal"],"exampleFix":"// before: blind retry after failure\nwhile (!success) { success = txn.commit(); } // may hit already-committed\n// after: check status first\nTxnStatus s = txnMeta.txnStatus();\nif (s == TxnStatus.OPEN) txn.commit(); else log(\"txn already \" + s);","handlingStrategy":"try-catch","validationCode":"// check current status before requesting a transition\nTxnStatus cur = getTxnStatus(txnId); // admin/coordinator API\nboolean legal = (cur == TxnStatus.OPEN && target == TxnStatus.COMMITTING)\n             || (cur == TxnStatus.OPEN && target == TxnStatus.ABORTING);\nif (!legal) skipTransition(cur);","typeGuard":null,"tryCatchPattern":"try {\n  txnMeta.updateTxnStatus(newStatus, expectedStatus);\n} catch (InvalidTxnStatusException e) {\n  log.warn(\"txn {} already transitioned (raced or terminal); skipping\", txnMeta.id());\n  // idempotent handling: treat commit/abort retries on terminal states as success\n}","preventionTips":["Make commit/abort idempotent — ignore transitions from terminal states","Pass the correct expectedStatus so races fail fast instead of corrupting state","Avoid manual retries of commit/abort after a previous attempt succeeded","Keep coordinator and client versions aligned"],"tags":["transaction","state-machine","pulsar"],"backgroundTag":"invalid-state-transition","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}