{"record":{"id":"9412113021bf58bd","repo":"tursodatabase/turso","slug":"clear-savepoints-should-not-fail-for-attached-db","errorCode":null,"errorMessage":"clear_savepoints should not fail for attached DB","messagePattern":"clear_savepoints should not fail for attached DB","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"core/storage/pager.rs","lineNumber":3474,"sourceCode":"    }\n\n    pub fn holds_write_lock(&self) -> bool {\n        let Some(wal) = self.wal.as_ref() else {\n            return false;\n        };\n        wal.holds_write_lock()\n    }\n\n    /// Rollback and clean up an attached database pager's transaction.\n    /// Unlike rollback_tx, this doesn't modify connection-level state.\n    pub fn rollback_attached(&self) {\n        let Some(wal) = self.wal.as_ref() else {\n            return;\n        };\n        let is_write = wal.holds_write_lock();\n        if is_write {\n            self.clear_savepoints()\n                .expect(\"clear_savepoints should not fail for attached DB\");\n            // Clear dirty pages and page cache before releasing the write lock\n            self.clear_page_cache(true);\n            self.dirty_pages.write().clear();\n            self.reset_internal_states();\n            self.set_schema_cookie(None);\n            wal.rollback(None);\n            wal.end_write_tx();\n        } else {\n            self.cleanup_read_tx();\n        }\n        if wal.holds_read_lock() {\n            wal.end_read_tx();\n        }\n    }\n\n    /// Reads a page from disk (either WAL or DB file) bypassing page-cache\n    #[tracing::instrument(skip_all, level = Level::DEBUG)]\n    /// Reads a page without going through the page cache. The read is","sourceCodeStart":3456,"sourceCodeEnd":3492,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/core/storage/pager.rs#L3456-L3492","documentation":"Pager::rollback_attached() is the attached-database variant of transaction rollback: it calls clear_savepoints().expect(\"clear_savepoints should not fail for attached DB\") on the same memory-IO-only subjournal. As with rollback_tx, a failure here is treated as an impossible condition that must not be silently swallowed while the attached db's write lock is still held.","triggerScenarios":"Rolling back a write transaction on an ATTACHed database (or engine-driven cleanup of attached pagers) when the in-memory subjournal write/remove errors.","commonSituations":"Multi-database applications using ATTACH with savepoints, memory pressure during attached-db rollbacks, engine regressions in subjournal handling for attached pagers.","solutions":["Report to Turso with the ATTACH/rollback sequence","Detach or reopen the connection and retry; uncommitted attached-db state is recoverable from disk","Run integrity checks on the attached databases afterwards","Keep write transactions on attached databases short to shrink subjournal usage"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"let result = std::panic::catch_unwind(AssertUnwindSafe(|| handle_rollback(&conn)));\nif result.is_err() {\n    // Attached-db rollback cleanup failed: drop the whole connection and reopen\n    // both the main and attached databases from disk.\n    drop(conn);\n    let conn = reopen_with_attach(&main_path, &attached_path, &io)?;\n}","preventionTips":["Keep transactions spanning ATTACHed databases short to limit subjournal growth","Watch memory pressure when many savepoints are open across attached databases","Reopen connections (main + attached) after any rollback panic rather than retrying in place","Verify attached databases with integrity checks after recovery"],"tags":["pager","rollback","attached-db","savepoint","panic"],"backgroundTag":"rollback-cleanup-failure","analyzedSha":"492c4a71cd7c2649e7df83da1471b74f4b1c7aa9","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}