{"record":{"id":"8ecb90550d6a0731","repo":"tursodatabase/turso","slug":"page-evicted-between-scan-and-prepare","errorCode":null,"errorMessage":"page evicted between scan and prepare","messagePattern":"page evicted between scan and prepare","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"core/storage/pager.rs","lineNumber":4552,"sourceCode":"                    let page_sz = self.get_page_size_unchecked();\n                    let mut commit_info = self.commit_info.write();\n                    let mut cache = self.page_cache.write();\n\n                    'inner: loop {\n                        let cursor = commit_info.page_source_cursor;\n                        if cursor >= commit_info.page_sources.len() {\n                            break 'inner;\n                        }\n\n                        let total = commit_info.page_sources.len();\n                        let is_last = cursor + 1 >= total;\n                        // Linear consumption, no lookup required\n                        let page = match &commit_info.page_sources[cursor] {\n                            PageSource::Cached(page_id) => {\n                                let page_key = PageCacheKey::new(*page_id);\n                                cache\n                                    .get(&page_key)?\n                                    .expect(\"page evicted between scan and prepare\")\n                            }\n                            PageSource::Evicted(page) => page.clone(),\n                        };\n                        // Defensive check: prepare_frames will read page contents,\n                        // which panics if the buffer is not loaded. If we got here\n                        // with an unloaded page (e.g. an evicted dirty page whose\n                        // backing WAL frame was truncated by a savepoint rollback),\n                        // surface an internal error instead of panicking.\n                        if !page.is_loaded() {\n                            return Err(LimboError::InternalError(format!(\n                                \"dirty page {} has no buffer loaded at commit time\",\n                                page.get().id()\n                            ))\n                            .into());\n                        }\n                        turso_assert!(\n                            page.get().overflow_cells.is_empty(),\n                            \"dirty page still has overflow cells at commit time\",","sourceCodeStart":4534,"sourceCodeEnd":4570,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/core/storage/pager.rs#L4534-L4570","documentation":"Commit preparation walks commit_info.page_sources; entries recorded as PageSource::Cached(page_id) are re-fetched from the page cache with expect(\"page evicted between scan and prepare\"), relying on pages being pinned between the commit scan and frame preparation. The panic fires when such a page is no longer in the cache - an eviction raced the commit.","triggerScenarios":"Committing a large dirty set while cache pressure evicts a page that was recorded as cached; or pin-count bookkeeping dropped a pin early (a statement released pages mid-commit). The neighboring unloaded-page check converts the milder variant into an InternalError, but the cache miss itself panics here.","commonSituations":"Big transactions near the cache limit, concurrent statements finishing during commit, engine regressions in pin discipline.","solutions":["Report to Turso with a reproduction - pin-discipline bug, not caller-fixable","Raise the page cache size so the commit's dirty set fits without eviction pressure","Commit in smaller batches to shrink the page_sources list","Run PRAGMA integrity_check after any occurrence to confirm the database is intact"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Application-level: keep the per-transaction dirty set comfortably below the\n// page cache so the commit scan cannot lose pages to eviction:\n//   - raise cache size (PRAGMA cache_size / engine config), or\n//   - commit in smaller batches.\nconn.execute(\"PRAGMA cache_size = -262144\", ())?; // example: ~256MB cache","typeGuard":null,"tryCatchPattern":"let result = std::panic::catch_unwind(AssertUnwindSafe(|| conn.execute(\"COMMIT\", ())));\nif result.is_err() {\n    conn.close().ok();\n    // Uncommitted work is lost by design; reopen and verify:\n    //   PRAGMA integrity_check;\n}","preventionTips":["Size the page cache above your largest transaction's dirty set","Split very large transactions into batches with intermediate commits","Avoid concurrent statement churn during COMMIT of big transactions","Run integrity_check after any commit-path panic before reusing the file"],"tags":["pager","commit","page-cache","eviction","panic"],"backgroundTag":"cache-eviction-race","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"}