{"record":{"id":"4ff68e0de9512d4b","repo":"facebook/flow","slug":"an-active-transaction-may-only-be-committed-once","errorCode":null,"errorMessage":"an active transaction may only be committed once","messagePattern":"an active transaction may only be committed once","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"rust_port/crates/flow_heap/src/transaction.rs","lineNumber":113,"sourceCode":"            heap,\n            overlay: HeapOverlay::new(),\n            committed: RwLock::new(Some(guard)),\n        })))\n    }\n\n    pub fn handle(&self) -> Arc<Transaction> {\n        self.0\n            .as_ref()\n            .expect(\"an active transaction cannot be used after commit\")\n            .dupe()\n    }\n\n    /// Publishes the overlay into the heap the transaction was opened on.\n    pub fn commit(mut self) {\n        let transaction = self\n            .0\n            .take()\n            .expect(\"an active transaction may only be committed once\");\n        let destination = transaction.committed_heap();\n        transaction.commit(&destination);\n    }\n}\n\nimpl Drop for ActiveTransaction {\n    fn drop(&mut self) {\n        if let Some(transaction) = self.0.as_ref() {\n            transaction.release();\n        }\n    }\n}\n\n/// Borrows the committed heap for the duration of one read. Produced by\n/// [`Transaction::latest_reader`] / [`Transaction::committed_reader`].\npub struct HeapAccess<'a> {\n    state: CommittedStateAccess<'a>,\n    overlay: Option<&'a HeapOverlay>,","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/facebook/flow/blob/5c865861998a8ccb7dbc82b0c1f511e9ef60c3d9/rust_port/crates/flow_heap/src/transaction.rs#L95-L131","documentation":"ActiveTransaction::commit calls self.0.take(), which empties the Option holding the transaction; the expect panics if it is already None. This means commit() (or Drop, which also consumes the transaction) was invoked twice on the same ActiveTransaction — an API misuse, since committing is a one-shot, move-consuming operation. The library enforces the single-commit invariant with this panic.","triggerScenarios":"Calling .commit() on the same ActiveTransaction value twice (only possible if a copy/clone of the guard was retained, or commit is called after the guard was already consumed or dropped); tests like commit_rejects_retained_transaction_handles exercise exactly this retained-handle case.","commonSituations":"Storing the ActiveTransaction in two places (e.g. cloning a wrapper that shares the Option via interior mutability such as Rc<RefCell<>>); calling commit() inside a helper and again at the call site; calling commit() after an early-return path already dropped the guard.","solutions":["Commit each ActiveTransaction exactly once: consume it by value (tx.commit()) and do not keep or re-create a handle afterwards.","Restructure code so the guard is moved into a single code path; use the returned/dropped state to get a fresh transaction if needed (drop rolls back and the next get returns a fresh overlay).","Use Option::take yourself or check is_some() before a second logical commit, converting the panic into a controlled error.","Avoid wrappers with interior mutability around ActiveTransaction; rely on Rust's move semantics to make double commit unrepresentable."],"exampleFix":"// before\nlet tx = heap.transaction();\ntx.commit();\ntx.commit(); // panics: transaction already consumed\n// after\nlet tx = heap.transaction();\ntx.commit(); // exactly once; open a new transaction for more work\nlet tx2 = heap.transaction();\ntx2.commit();","handlingStrategy":"type-guard","validationCode":"fn can_commit(tx: &Option<ActiveTransaction>) -> bool {\n    tx.is_some() // only commit when a transaction handle is present\n}","typeGuard":"fn is_active(tx: &ActiveTransaction) -> bool { !tx.is_consumed() } // or track via Option<ActiveTransaction> and check is_some()","tryCatchPattern":"let result = std::panic::catch_unwind(|| { tx.commit(); });\nif result.is_err() {\n    eprintln!(\"transaction already committed; opening a fresh one\");\n    let tx = heap.transaction();\n    tx.commit();\n}","preventionTips":["Model the transaction as Option<ActiveTransaction> and take() it before committing.","Never store transaction guards in cloneable/shared containers (Rc, RefCell).","Remember Drop performs an implicit rollback: after any drop, open a new transaction.","Keep commit calls in one clearly-owned code path per transaction."],"tags":["transaction","invariant","double-commit","panic","state-management"],"backgroundTag":"invalid-state-transition","analyzedSha":"5c865861998a8ccb7dbc82b0c1f511e9ef60c3d9","analyzedAt":"2026-09-08T04:32:53.179Z","contentChangedAt":"2026-09-08T04:32:53.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}