clockworklabs/SpacetimeDB · error · anyhow::Error

begin mutable transaction while one is already active

Error message

begin mutable transaction while one is already active

What it means

The dst engine target allows at most one active mutable transaction: execute(Interaction::BeginMutTx) asserts active_mut_tx.is_none(). This error means BeginMutTx was issued while a previous mutable transaction was still open — no CommitTx (or Replay, which implicitly discards the active tx) happened in between.

Source

Thrown at crates/dst/src/engine.rs:195

            tables.push(TableDelta {
                table,
                inserts,
                deletes,
                truncated: entry.truncated,
            });
        }

        tables.sort_by_key(|delta| delta.table);
        CommitDelta { tables }
    }

    pub fn execute(&mut self, interaction: &Interaction) -> anyhow::Result<Observation> {
        tracing::debug!(?interaction, "executing interaction");

        let observation = match interaction {
            Interaction::BeginMutTx => {
                anyhow::ensure!(
                    self.active_mut_tx.is_none(),
                    "begin mutable transaction while one is already active"
                );
                let db = self
                    .db
                    .as_ref()
                    .ok_or_else(|| anyhow::anyhow!("database is not open"))?;
                self.active_mut_tx = Some(db.begin_mut_tx(IsolationLevel::Serializable, Workload::Internal));
                Ok(Observation::BeganMutTx)
            }
            Interaction::Insert { table, row } => {
                let table_id = self.table_ids[*table];
                let bytes = row_to_bytes(row);
                let db = self
                    .db
                    .as_ref()
                    .ok_or_else(|| anyhow::anyhow!("database is not open"))?;
                let tx = self

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Fix the generator's state machine: only emit BeginMutTx when no transaction is active
  2. Emit CommitTx (or Replay) before the next BeginMutTx
  3. Track active-transaction state in the driver and assert the invariant between steps

Example fix

// before: two begins, no commit in between
vec![BeginMutTx, Insert { .. }, BeginMutTx, Delete { .. }, CommitTx]

// after: one transaction at a time
vec![BeginMutTx, Insert { .. }, CommitTx, BeginMutTx, Delete { .. }, CommitTx]
Defensive patterns

Strategy: validation

Validate before calling

// Generator-side state check before emitting BeginMutTx
fn can_begin_mut_tx(tx_active: bool) -> bool {
    !tx_active
}

Prevention

When it happens

Trigger: A generated interaction sequence like BeginMutTx, Insert, BeginMutTx — the workload generator emitted a second begin without an intervening CommitTx. Note Interaction::Replay silently drops the active transaction, so it also resets this state.

Common situations: Bugs in DST workload generators (WorkloadGen) or hand-written interaction scripts; state-machine modeling that forgets the tx-open edge.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/d733671f684bc7de. Report an issue: GitHub.