tursodatabase/turso · critical

in practice, clear_savepoints() should never fail as it uses

Error message

in practice, clear_savepoints() should never fail as it uses memory IO

What it means

Pager::rollback_tx(), while rolling back a write transaction, calls clear_savepoints().expect("in practice, clear_savepoints() should never fail as it uses memory IO"). The subjournal is backed by MemoryIO so its writes complete inline; a failure there (allocation or subjournal bug) is converted into a loud panic instead of silently discarding savepoint state.

Source

Thrown at core/storage/pager.rs:3412

    #[instrument(skip_all, level = Level::DEBUG)]
    pub fn rollback_tx(&self, connection: &Connection) {
        if connection.is_nested_stmt() {
            // Parent statement will handle the transaction rollback.
            return;
        }
        let Some(wal) = self.wal.as_ref() else {
            // TODO: Unsure what the semantics of "end_tx" is for in-memory databases, ephemeral tables and ephemeral indexes.
            return;
        };
        let (is_write, schema_did_change) = match connection.get_tx_state() {
            TransactionState::Write { schema_did_change } => (true, schema_did_change),
            _ => (false, false),
        };
        tracing::trace!("rollback_tx(schema_did_change={})", schema_did_change);
        if is_write {
            self.clear_savepoints()
                .expect("in practice, clear_savepoints() should never fail as it uses memory IO");
            // IMPORTANT: rollback() must be called BEFORE end_write_tx() releases the write_lock.
            // Otherwise, another thread could commit new frames to frame_cache between
            // end_write_tx() and rollback(), and rollback() would incorrectly remove them.
            self.rollback(schema_did_change, connection, is_write);
            wal.end_write_tx();
        } else {
            self.rollback(schema_did_change, connection, is_write);
        }
        wal.end_read_tx();
    }

    pub(crate) fn cleanup_read_tx(&self) {
        let Some(wal) = self.wal.as_ref() else {
            return;
        };
        self.reset_internal_states();
        if wal.holds_read_lock() {
            wal.end_read_tx();

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report to Turso with the transaction shape - memory-IO failure here is an engine defect
  2. Retry the rollback on a fresh connection; pre-commit on-disk state is unchanged so reopening is safe
  3. Run PRAGMA integrity_check after reopening
  4. Shorten write transactions / reduce savepoint churn to lower subjournal memory footprint
Defensive patterns

Strategy: fallback

Try / catch

let result = std::panic::catch_unwind(AssertUnwindSafe(|| conn.execute("ROLLBACK", ())));
if result.is_err() {
    // Do not reuse the connection: discard it and reopen from disk.
    // WAL recovery replays only committed frames, so on-disk state stays consistent.
    drop(conn);
    let conn = reopen_database(&path, &io)?;
}

Prevention

When it happens

Trigger: ROLLBACK of a write transaction that had opened savepoints/subjournal entries, when the in-memory subjournal operation returns an error - memory exhaustion inside MemoryIO or a subjournal format regression.

Common situations: Severe memory pressure during long write transactions, engine regressions in subjournal bookkeeping, retrying an interrupted rollback on the same pager.

Related errors


AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20). Data as JSON: /api/errors/edd880e59302899c. Report an issue: GitHub.