{"record":{"id":"e113252309bcbe2d","repo":"libnyanpasu/clash-nyanpasu","slug":"state-transaction-dropped-before-commit-or-rollbac","errorCode":null,"errorMessage":"state transaction dropped before commit or rollback completed","messagePattern":"state transaction dropped before commit or rollback completed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"backend/nyanpasu-core/src/state/transaction.rs","lineNumber":123,"sourceCode":"impl<T> Drop for RollbackGuard<T>\nwhere\n    T: Clone + Send + Sync + 'static,\n{\n    fn drop(&mut self) {\n        let Some(data) = self.data.take() else {\n            return;\n        };\n\n        tracing::warn!(\n            change_id = ?data.change.id,\n            \"state transaction dropped before commit or rollback completed; notifying rollback subscribers\"\n        );\n\n        block_on_anywhere(notify_rollback(\n            &data.change,\n            &data.subscribers,\n            data.notify_strategy,\n            RollbackReason::CoordinatorError(Arc::new(anyhow::anyhow!(\n                \"state transaction dropped before commit or rollback completed\"\n            ))),\n        ));\n    }\n}\n\nasync fn notify_rollback<T>(\n    change: &StateChange<T>,\n    subscribers: &[ArcStateSubscriber<T>],\n    notify_strategy: NotifyStrategy,\n    reason: RollbackReason,\n) where\n    T: Clone + Send + Sync + 'static,\n{\n    match notify_strategy {\n        NotifyStrategy::Parallel => {\n            notify::NotifyExecutor::<T, state::RolledBack, notify::Parallel>::notify_all(\n                change,","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/nyanpasu-core/src/state/transaction.rs#L105-L141","documentation":"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.","triggerScenarios":"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`).","commonSituations":"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.","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)."],"exampleFix":"// before: early return leaks the prepared transaction\nlet tx = state.prepare(change).await?;\nvalidate(&change)?; // error -> tx dropped -> coordinator-error rollback\n// after: explicit rollback on error\nlet tx = state.prepare(change).await?;\nif let Err(e) = validate(&change) {\n    tx.rollback().await;\n    return Err(e);\n}\ntx.commit().await?;","handlingStrategy":"try-catch","validationCode":"// assert you never abandon a transaction: wrap prepare/commit in one function\nasync fn run_tx(state: &StateManager, change: StateChange) -> anyhow::Result<()> {\n    let tx = state.prepare(change).await?;\n    let result = apply(&tx).await;\n    match result {\n        Ok(v) => { tx.commit().await?; Ok(v) }\n        Err(e) => { tx.rollback().await; Err(e) }\n    }\n}","typeGuard":"fn transaction_abandoned(reason: &RollbackReason) -> bool {\n    matches!(reason, RollbackReason::CoordinatorError(_))\n}","tryCatchPattern":"// observe coordinator-error rollbacks in subscriber handlers\nfn on_rollback(reason: RollbackReason) {\n    if let RollbackReason::CoordinatorError(e) = &reason {\n        tracing::error!(\"transaction dropped without explicit commit/rollback: {e:#}\");\n        metrics::increment!(\"state.tx.abandoned\");\n    }\n}","preventionTips":["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."],"tags":["rust","state-management","transaction","drop-safety","rollback"],"backgroundTag":"invalid-state-transition","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}