Assertion Failure in pager_playback_one_page During Transaction Rollback with Max Page Constraints


Root Cause: Transaction Rollback Collision with Page Truncation During Incremental Vacuum

The assertion failure in pager_playback_one_page arises from an invalid assumption about the availability of database pages during transaction rollback when incremental vacuum operations and strict page count limits are enforced. This occurs when SQLite attempts to replay journaled changes to pages that have been truncated due to max_page_count constraints, resulting in a null page pointer (pPg) in a disk-based database context. The pager_playback_one_page function expects either a valid page pointer or confirmation that the database operates in memory (which it does not), triggering the assertion failure. The root cause lies in the interplay between incremental vacuum logic, page reuse policies, and transaction journal playback under resource-constrained configurations.


Critical Triggers: Page Reclamation Logic, Journal Playback Sequence, and Vacuum Constraints

The failure is triggered by three interconnected factors:

  1. Incremental Vacuum and Page Truncation Conflicts: When PRAGMA incremental_vacuum=0 is set, SQLite defers page reclamation. However, PRAGMA max_page_count=2 forces immediate truncation of excess pages. This creates a race condition where pages required for transaction rollback may have been permanently removed.
  2. Transaction Journal Dependency on Stale Pages: The SAVEPOINT mechanism journals changes to pages that might later be truncated. During COMMIT or rollback, the pager attempts to replay these changes, assuming the pages still exist.
  3. Auto-Vacuum Mode 2 Fragmentation: With auto_vacuum=2, SQLite allows free pages to accumulate until explicitly vacuumed. The combination of deferred vacuuming and forced truncation via max_page_count creates an inconsistent page map, where the pager’s internal state references pages that no longer exist.

This triad of constraints destabilizes the pager’s ability to guarantee page availability during critical recovery operations, violating the assertion’s assumptions.


Resolution Pathway: Page Map Validation, Vacuum Sequence Adjustment, and Assertion Safeguards

Step 1: Validate Page Existence During Journal Playback
Modify the pager logic in pager_playback_one_page to explicitly check whether a page referenced in the journal still exists in the database file before attempting to access it. If the page has been truncated due to max_page_count, discard the journal entry and mark the transaction as irrecoverable. This avoids dereferencing a null page pointer and suppresses the assertion failure.

Step 2: Enforce Coherent Vacuum and Page Limit Interactions
Adjust the incremental vacuum logic to prioritize page truncation operations before journal playback. When max_page_count is set, SQLite should invalidate journal entries that reference pages beyond the new limit. This ensures the pager does not attempt to replay changes to pages that have been forcibly removed.

Step 3: Strengthen Assertion Guards with Page Existence Checks
Augment the assertion assert(pPg || !MEMDB) with additional validation:

assert(pPg || !MEMDB || sqlite3PagerPageTruncated(pPager, pgno));

Here, sqlite3PagerPageTruncated() would verify whether the page number (pgno) exists in the current database file. This accounts for scenarios where pages are legally removed due to external constraints like max_page_count.

Step 4: Refactor max_page_count Enforcement to Invalidate Dependent Journals
When PRAGMA max_page_count reduces the database size, SQLite should automatically release or invalidate all savepoints and journals that reference pages beyond the new limit. This prevents the pager from attempting to replay operations on non-existent pages.

Step 5: Introduce Robust Error Handling for Full-Disk Scenarios
The "database or disk is full" error should trigger a rollback of the current transaction and release all associated savepoints. This prevents subsequent operations from relying on a corrupted journal state.

Step 6: Update Auto-Vacuum Documentation with Page Limit Caveats
Explicitly warn users that combining auto_vacuum=2 with max_page_count requires manual vacuuming before altering page limits. Provide code samples for safe page limit adjustments:

PRAGMA incremental_vacuum; -- Reclaim free pages
PRAGMA max_page_count=N;   -- Set new limit after vacuum

Step 7: Backport Page Truncation Safeguards to Legacy Branches
For SQLite versions affected by the bisected regression (post commit aa512f72cf5adfec), apply patches that decouple journal playback from page truncation logic. This ensures compatibility with environments where max_page_count is dynamically reduced.

Step 8: Implement Integration Tests for Page Limit Edge Cases
Develop test cases that combine auto_vacuum=2, incremental_vacuum, max_page_count, and nested savepoints. These tests should validate that journal playback gracefully handles page truncation events without triggering assertions.

Final Code Fix Example
In pager_playback_one_page, replace the assertion with:

if( !pPg && !MEMDB ){
  if( sqlite3PagerPageTruncated(pPager, pgno) ){
    sqlite3_log(SQLITE_CORRUPT, "Page %d truncated during playback", pgno);
    return SQLITE_CORRUPT;
  }
  assert( pPg || !MEMDB );
}

This bypasses the assertion if the page was legally truncated and returns an actionable error instead of crashing.

By addressing the page lifecycle conflicts between vacuum operations, transaction journals, and page limits, this multi-pronged approach eliminates the assertion failure while preserving database integrity under constrained resource scenarios.

Related Guides

Leave a Reply

Your email address will not be published. Required fields are marked *