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
- 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.
- Check for panics in the code between prepare and commit; fix the panicking code or catch/unwind safely with an explicit rollback.
- If the drop is intentional (cancellation), call `rollback()` explicitly first so subscribers get a normal rollback notification instead of CoordinatorError.
- 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
- Never `?`-return between prepare and commit/rollback; route all exits through explicit rollback.
- Use a drop-guard or dedicated run helper that guarantees terminal transition of every transaction.
- Fix panics in the prepare..commit window; panic unwinding is a common source of abandoned transactions.
- In tests, prefer explicit cancellation APIs over dropping prepared transactions.
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
- compensation fenced by diverged target at {}
- effect function failed: {error:#?}
- effect function failed: {e:#?}
- write config timed out after {timeout:?}
- failed to join notify task: {error}
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/e113252309bcbe2d.
Report an issue: GitHub.