{"record":{"id":"707354ffde957e98","repo":"clockworklabs/SpacetimeDB","slug":"commit-without-active-mutable-transaction","errorCode":null,"errorMessage":"commit without active mutable transaction","messagePattern":"commit without active mutable transaction","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/dst/src/engine.rs","lineNumber":244,"sourceCode":"            }\n            Interaction::Delete { table, row } => {\n                let table_id = self.table_ids[*table];\n                let db = self\n                    .db\n                    .as_ref()\n                    .ok_or_else(|| anyhow::anyhow!(\"database is not open\"))?;\n                let tx = self\n                    .active_mut_tx\n                    .as_mut()\n                    .ok_or_else(|| anyhow::anyhow!(\"delete without active mutable transaction\"))?;\n                db.delete_by_rel(tx, table_id, [row.clone()]);\n                Ok(Observation::Deleted)\n            }\n            Interaction::CommitTx => {\n                let tx = self\n                    .active_mut_tx\n                    .take()\n                    .ok_or_else(|| anyhow::anyhow!(\"commit without active mutable transaction\"))?;\n                let db = self\n                    .db\n                    .as_ref()\n                    .ok_or_else(|| anyhow::anyhow!(\"database is not open\"))?;\n                let Some((_tx_offset, tx_data, _tx_metrics, _reducer)) = db.commit_tx(tx)? else {\n                    anyhow::bail!(\"commit produced no transaction data\");\n                };\n                Ok(Observation::Committed {\n                    delta: self.commit_delta_from_tx_data(&tx_data),\n                })\n            }\n            Interaction::Replay => {\n                let _ = self.active_mut_tx.take();\n                self.reopen_from_commitlog()?;\n                Ok(Observation::Replayed {\n                    state: self.count_state()?,\n                })\n            }","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/6dee26c6efc2856793e12b148a59742964f5d783/crates/dst/src/engine.rs#L226-L262","documentation":"Thrown by the dst (deterministic simulation testing) harness in EngineTarget::execute when an Interaction::CommitTx arrives while active_mut_tx is None. The harness drives RelationalDB as a strict state machine (BeginMutTx -> Insert/Delete -> CommitTx); CommitTx consumes the transaction with Option::take, so committing without a matching begin leaves nothing to commit.","triggerScenarios":"Executing Interaction::CommitTx without a prior successful Interaction::BeginMutTx; issuing CommitTx twice in a row (the first take() empties the slot); issuing CommitTx after Interaction::Replay, which silently discards any open transaction via `let _ = self.active_mut_tx.take()`.","commonSituations":"A custom workload/interaction generator that emits unbalanced begin/commit sequences; a generator that keeps producing interactions after a Replay step without re-opening a transaction; a failed earlier interaction that already consumed the transaction.","solutions":["Emit Interaction::BeginMutTx before every CommitTx (one commit per begin)","After an Interaction::Replay step, re-issue BeginMutTx before any Insert/Delete/Commit, since Replay drops the open transaction","Add an invariant to the WorkloadGen: only generate CommitTx when a transaction is open","If you wrap EngineTarget in a custom driver, mirror the tx-open flag from observations and assert begin/commit balance at sequence end"],"exampleFix":"// before: generator emits an unbalanced commit\nseq.push(Interaction::CommitTx);\n\n// after: every commit is paired with a begin\nseq.push(Interaction::BeginMutTx);\nseq.push(Interaction::CommitTx);","handlingStrategy":"validation","validationCode":"// Mirror tx state from observations before issuing CommitTx\nlet tx_open = match last_observation {\n    Some(Observation::BeganMutTx) => true,\n    Some(Observation::Committed { .. }) | Some(Observation::Replayed { .. }) => false,\n    other => other.is_some(), // Insert/Delete/observations preserve state\n};\nif !tx_open {\n    target.execute(&Interaction::BeginMutTx)?;\n}","typeGuard":"fn can_commit(last: Option<&Observation>) -> bool {\n    !matches!(last, None | Some(Observation::Committed { .. }) | Some(Observation::Replayed { .. }))\n}","tryCatchPattern":"match target.execute(&Interaction::CommitTx) {\n    Ok(obs) => { /* continue */ }\n    Err(e) if e.to_string().starts_with(\"commit without active mutable transaction\") => {\n        // generator sequence bug: fix the sequence, do not retry blindly\n        return Err(e.context(\"unbalanced BeginMutTx/CommitTx in generated workload\"));\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Track begin/commit balance as an explicit generator invariant","Treat Replay as a sequence reset: always begin a new transaction afterwards","Abort the run on the first interaction error instead of continuing"],"tags":["rust","spacetimedb","testing","transaction","state-machine"],"backgroundTag":"invalid-state-transition","analyzedSha":"6dee26c6efc2856793e12b148a59742964f5d783","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}