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
- Report to Turso with the transaction shape - memory-IO failure here is an engine defect
- Retry the rollback on a fresh connection; pre-commit on-disk state is unchanged so reopening is safe
- Run PRAGMA integrity_check after reopening
- 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
- Keep write transactions short so the savepoint/subjournal set stays small at rollback
- Monitor memory usage; the subjournal is memory-backed and allocation failure is the realistic trigger
- Never continue using a connection after a rollback-path panic - reopen instead
- Run integrity_check after reopening
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
- clear_savepoints should not fail for attached DB
- subjournal must be opened
- buffer not loaded
- DB should not be initialized and should not do any IO
- Page size too small, a ptrmap page cannot map any db pages.
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/edd880e59302899c.
Report an issue: GitHub.