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
- 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
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
- 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
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
- Expected MessageIdV5, got: + messageId.getClass()
- Cannot start the service once it was stopped
- The topic has a max partition index of %d, the number of par
- entryFilterNames can't be empty. To remove entry filters use
- The offloadPolicies must be specified for namespace offload.
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/40d0e5862da739d8.
Report an issue: GitHub.