clockworklabs/SpacetimeDB · error · anyhow::Error
commit without active mutable transaction
Error message
commit without active mutable transaction
What it means
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.
Source
Thrown at crates/dst/src/engine.rs:244
}
Interaction::Delete { table, row } => {
let table_id = self.table_ids[*table];
let db = self
.db
.as_ref()
.ok_or_else(|| anyhow::anyhow!("database is not open"))?;
let tx = self
.active_mut_tx
.as_mut()
.ok_or_else(|| anyhow::anyhow!("delete without active mutable transaction"))?;
db.delete_by_rel(tx, table_id, [row.clone()]);
Ok(Observation::Deleted)
}
Interaction::CommitTx => {
let tx = self
.active_mut_tx
.take()
.ok_or_else(|| anyhow::anyhow!("commit without active mutable transaction"))?;
let db = self
.db
.as_ref()
.ok_or_else(|| anyhow::anyhow!("database is not open"))?;
let Some((_tx_offset, tx_data, _tx_metrics, _reducer)) = db.commit_tx(tx)? else {
anyhow::bail!("commit produced no transaction data");
};
Ok(Observation::Committed {
delta: self.commit_delta_from_tx_data(&tx_data),
})
}
Interaction::Replay => {
let _ = self.active_mut_tx.take();
self.reopen_from_commitlog()?;
Ok(Observation::Replayed {
state: self.count_state()?,
})
}View on GitHub (pinned to 6dee26c6ef)
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
Example fix
// before: generator emits an unbalanced commit seq.push(Interaction::CommitTx); // after: every commit is paired with a begin seq.push(Interaction::BeginMutTx); seq.push(Interaction::CommitTx);
Defensive patterns
Strategy: validation
Validate before calling
// Mirror tx state from observations before issuing CommitTx
let tx_open = match last_observation {
Some(Observation::BeganMutTx) => true,
Some(Observation::Committed { .. }) | Some(Observation::Replayed { .. }) => false,
other => other.is_some(), // Insert/Delete/observations preserve state
};
if !tx_open {
target.execute(&Interaction::BeginMutTx)?;
} Type guard
fn can_commit(last: Option<&Observation>) -> bool {
!matches!(last, None | Some(Observation::Committed { .. }) | Some(Observation::Replayed { .. }))
} Try / catch
match target.execute(&Interaction::CommitTx) {
Ok(obs) => { /* continue */ }
Err(e) if e.to_string().starts_with("commit without active mutable transaction") => {
// generator sequence bug: fix the sequence, do not retry blindly
return Err(e.context("unbalanced BeginMutTx/CommitTx in generated workload"));
}
Err(e) => return Err(e),
} Prevention
- 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
When it happens
Trigger: 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()`.
Common situations: 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.
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
- begin mutable transaction while one is already active
- insert without active mutable transaction
- delete without active mutable transaction
- cannot serialize refs without a typespace
- database is not open
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/707354ffde957e98.
Report an issue: GitHub.