apache/pulsar · error · InvalidTxnStatusException

Transaction `${txnID}` CANNOT transaction from status ${txnS

Error message

Transaction `${txnID}` CANNOT transaction from status ${txnStatus} to ${newStatus}

What it means

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.

Source

Thrown at pulsar-transaction/coordinator/src/main/java/org/apache/pulsar/transaction/coordinator/impl/TxnMetaImpl.java:149

        return this;
    }

    /**
     * Update the transaction stats from the <tt>newStatus</tt> only when
     * the current status is the expected <tt>expectedStatus</tt>.
     *
     * @param newStatus the new transaction status
     * @param expectedStatus the expected transaction status
     * @return the transaction itself.
     * @throws InvalidTxnStatusException
     */
    @Override
    public synchronized TxnMetaImpl updateTxnStatus(TxnStatus newStatus,
                                                    TxnStatus expectedStatus)
        throws InvalidTxnStatusException {
        checkTxnStatus(expectedStatus);
        if (!TransactionUtil.canTransitionTo(txnStatus, newStatus)) {
            throw new InvalidTxnStatusException(
                "Transaction `" + txnID + "` CANNOT transaction from status " + txnStatus + " to " + newStatus);
        }
        this.txnStatus = newStatus;
        return this;
    }

    @Override
    public long getOpenTimestamp() {
        return this.openTimestamp;
    }

    @Override
    public long getTimeoutAt() {
        return this.timeoutAt;
    }

    @Override
    public String getOwner() {

View on GitHub (pinned to 820761864e)

Solutions

  1. Check the transaction's current status (via coordinator admin API) before requesting a transition and only issue legal transitions
  2. Treat the transaction as finished if it is already in COMMITTED/ABORTED — do not retry commit/abort against terminal states
  3. Guard against client/timeout races: rely on the expectedStatus parameter so concurrent transitions fail fast and the loser backs off
  4. Keep client and broker versions consistent; older/newer TxnStatus handling can produce transitions the other side considers illegal

Example fix

// before: blind retry after failure
while (!success) { success = txn.commit(); } // may hit already-committed
// after: check status first
TxnStatus s = txnMeta.txnStatus();
if (s == TxnStatus.OPEN) txn.commit(); else log("txn already " + s);
Defensive patterns

Strategy: try-catch

Validate before calling

// check current status before requesting a transition
TxnStatus cur = getTxnStatus(txnId); // admin/coordinator API
boolean legal = (cur == TxnStatus.OPEN && target == TxnStatus.COMMITTING)
             || (cur == TxnStatus.OPEN && target == TxnStatus.ABORTING);
if (!legal) skipTransition(cur);

Try / catch

try {
  txnMeta.updateTxnStatus(newStatus, expectedStatus);
} catch (InvalidTxnStatusException e) {
  log.warn("txn {} already transitioned (raced or terminal); skipping", txnMeta.id());
  // idempotent handling: treat commit/abort retries on terminal states as success
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/40d0e5862da739d8. Report an issue: GitHub.