libnyanpasu/clash-nyanpasu · warning

state transaction dropped before commit or rollback complete

Error message

state transaction dropped before commit or rollback completed

What it means

When a state transaction (Transaction) is dropped without `commit()` or `rollback()` having completed, its Drop impl runs a last-resort rollback and notifies subscribers with RollbackReason::CoordinatorError carrying this message. The library throws it because abandoning a prepared transaction silently would leave the permit unreleased and subscribers unaware the change was discarded. It is a safety-net path — seeing it means normal transaction lifecycle discipline was violated.

Source

Thrown at backend/nyanpasu-core/src/state/transaction.rs:123

impl<T> Drop for RollbackGuard<T>
where
    T: Clone + Send + Sync + 'static,
{
    fn drop(&mut self) {
        let Some(data) = self.data.take() else {
            return;
        };

        tracing::warn!(
            change_id = ?data.change.id,
            "state transaction dropped before commit or rollback completed; notifying rollback subscribers"
        );

        block_on_anywhere(notify_rollback(
            &data.change,
            &data.subscribers,
            data.notify_strategy,
            RollbackReason::CoordinatorError(Arc::new(anyhow::anyhow!(
                "state transaction dropped before commit or rollback completed"
            ))),
        ));
    }
}

async fn notify_rollback<T>(
    change: &StateChange<T>,
    subscribers: &[ArcStateSubscriber<T>],
    notify_strategy: NotifyStrategy,
    reason: RollbackReason,
) where
    T: Clone + Send + Sync + 'static,
{
    match notify_strategy {
        NotifyStrategy::Parallel => {
            notify::NotifyExecutor::<T, state::RolledBack, notify::Parallel>::notify_all(
                change,

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Ensure every prepared transaction reaches an explicit `commit()` or `rollback()` on all code paths — use a guard or structure code so `?` returns cannot bypass them.
  2. Check for panics in the code between prepare and commit; fix the panicking code or catch/unwind safely with an explicit rollback.
  3. If the drop is intentional (cancellation), call `rollback()` explicitly first so subscribers get a normal rollback notification instead of CoordinatorError.
  4. Review subscriber handlers to make sure they tolerate a CoordinatorError rollback reason gracefully (the state is already reverted).

Example fix

// before: early return leaks the prepared transaction
let tx = state.prepare(change).await?;
validate(&change)?; // error -> tx dropped -> coordinator-error rollback
// after: explicit rollback on error
let tx = state.prepare(change).await?;
if let Err(e) = validate(&change) {
    tx.rollback().await;
    return Err(e);
}
tx.commit().await?;
Defensive patterns

Strategy: try-catch

Validate before calling

// assert you never abandon a transaction: wrap prepare/commit in one function
async fn run_tx(state: &StateManager, change: StateChange) -> anyhow::Result<()> {
    let tx = state.prepare(change).await?;
    let result = apply(&tx).await;
    match result {
        Ok(v) => { tx.commit().await?; Ok(v) }
        Err(e) => { tx.rollback().await; Err(e) }
    }
}

Type guard

fn transaction_abandoned(reason: &RollbackReason) -> bool {
    matches!(reason, RollbackReason::CoordinatorError(_))
}

Try / catch

// observe coordinator-error rollbacks in subscriber handlers
fn on_rollback(reason: RollbackReason) {
    if let RollbackReason::CoordinatorError(e) = &reason {
        tracing::error!("transaction dropped without explicit commit/rollback: {e:#}");
        metrics::increment!("state.tx.abandoned");
    }
}

Prevention

When it happens

Trigger: Dropping a Transaction while it is in the Prepared state — e.g. early `?` returns between prepare and commit, panics inside the committing function, or test/manual cancellation of a prepared transaction (see `test_with_pending_state_cancel_rolls_back_prepared_subscribers`).

Common situations: A caller prepares a state transaction, then an unrelated error path returns before calling commit/rollback; panic unwinding through code holding a prepared transaction; tests that cancel pending state to verify rollback behavior.

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 libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/e113252309bcbe2d. Report an issue: GitHub.