Tools: Unlocking and Journaling in SQLite

Tools: Unlocking and Journaling in SQLite

Source: Dev.to

The sqlite3OsUnlock API: Controlled Lock Downgrades ## Connection-Level vs Process-Level State (Again) ## Early Exit: Nothing to Do ## Lock Downgrades from Stronger Modes ## Unlocking to NOLOCK: Releasing the File Completely ## Why Unlocking Is Just as Careful as Locking ## Journaling: The Other Half of Transaction Safety ## The Rollback Journal ## Transient vs Retained Journals ## Idempotent Undo: A Powerful Property ## Handling Database Growth ## Tracking Journaled Pages ## Log Optimization: Freelist Pages ## A Critical Warning: No Database Aliasing ## Where This Takes Us Next ## References: Hello, I'm Maneshwar. I'm working on FreeDevTools online currently building "one place for all dev tools, cheat codes, and TLDRs" — a free, open-source hub where developers can quickly find and use tools without any hassle of searching all over the internet. In yesterday’s post, we followed the lock escalation path inside SQLite, all the way up to EXCLUSIVE locks, and saw how sqlite3OsLock carefully coordinates process wide and connection level state without ever letting native locks break isolation. Today’s reading completes that picture in two important ways: Together, these mechanisms close the loop on SQLite’s transaction safety model. Lock acquisition in SQLite is asymmetric — locks can only be strengthened using sqlite3OsLock. Releasing or weakening a lock is handled separately by the sqlite3OsUnlock API. The function signature is: On Unix platforms, this maps to posixUnlock in os_unix.c. A key design constraint is that sqlite3OsUnlock can only reduce lock strength to: SHARED, or NOLOCK Any attempt to increase lock strength must go through sqlite3OsLock. Just like lock acquisition, unlocking compares two states: This distinction ensures that one connection cannot accidentally undo another’s locks and process-wide invariants remain intact If the connection already holds a lock weaker than or equal to the requested downgrade SQLite immediately returns success. This avoids unnecessary work and keeps unlock paths fast. If the connection currently holds a lock stronger than SHARED, SQLite enters a lock downgrade path. This ensures that readers may safely proceed, writers are still excluded and no process-wide invariants are violated When the requested lock type is NOLOCK, SQLite is signaling that this connection is done with the file. This step is crucial: it ties together unlocking with lazy file closing, ensuring that file descriptors are only released when no thread still depends on the locks. Unlike many databases, SQLite cannot rely on the OS to “do the right thing” when unlocking. A premature unlock or close could: The sqlite3OsUnlock algorithm is deliberately conservative to prevent these outcomes. Locks alone prevent concurrent corruption. They do not protect against crashes, power failures or aborted transactions That responsibility belongs to journaling. A journal is a repository of recovery information that allows SQLite to restore the database to a consistent state. SQLite uses rollback journaling, not redo logging. For each database file, SQLite maintains one rollback journal (except for in-memory databases). The rollback journal stores only undo information, never redo data. By default, SQLite uses transient journaling: This behavior can be modified using: Each option trades durability, performance, and crash safety differently. Because SQLite logs full page images: If recovery is interrupted and restarted: This simplicity is one of SQLite’s greatest strengths. If a transaction adds new pages: SQLite handles this by: SQLite keeps an in-memory bitmap to track which pages have already been journaled. When a freelist leaf page is reused: SQLite optimizes by skipping journaling for such pages. SQLite assumes one database file → one canonical name Using hard links, symbolic links or renamed database files can result in: Similarly renaming a database without renaming its journal or interacting with a master journal is extremely dangerous. You have been warned — severely. At this point, we have all the building blocks: The next natural step is to study: That’s where SQLite turns all of this machinery into durable, crash-safe commits. My experiments and hands-on executions related to SQLite will live here: lovestaco/sqlite SQLite Database System: Design and Implementation. N.p.: Sibsankar Haldar, (n.d.). 👉 Check out: FreeDevTools Any feedback or contributors are welcome! It’s online, open-source, and ready for anyone to use. ⭐ Star it on GitHub: freedevtools Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: int sqlite3OsUnlock(unixFile *id, int locktype); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: int sqlite3OsUnlock(unixFile *id, int locktype); CODE_BLOCK: int sqlite3OsUnlock(unixFile *id, int locktype); CODE_BLOCK: PRAGMA journal_mode; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: PRAGMA journal_mode; CODE_BLOCK: PRAGMA journal_mode; - How locks are safely released or downgraded - How journaling guarantees atomicity and durability once writes begin - id->eFileLock → lock held by this specific database connection - id->inodeinfo->eFileLock → strongest lock held by the process on this inode - EXCLUSIVE → SHARED - RESERVED → SHARED - a read lock is placed on the SHARED_BYTES region - write locks on the PENDING and RESERVED regions are cleared - The shared-lock counter nShared in unixinodeinfo is decremented - If nShared reaches zero: all native locks on the file are cleared - all native locks on the file are cleared - The process-wide lock counter nLock is decremented - If nLock reaches zero: all lazy-close file descriptors are finally closed - all lazy-close file descriptors are finally closed - all native locks on the file are cleared - all lazy-close file descriptors are finally closed - silently drop another transaction’s lock - allow concurrent writers - corrupt the database - One write-transaction at a time - Journal created at the start of a write-transaction - Journal deleted (or truncated) when the transaction completes - journal file created per transaction - deleted on commit or rollback - truncate – keep file, truncate contents - persist – keep file, invalidate header - memory – journal stored entirely in RAM - off – journaling disabled (dangerous) - undo is done by blindly copying pages back - undo operations are idempotent - no compensating log records are needed - repeating undo causes no harm - there is no old value to log - recording the original database size in the journal header - truncating the database file back to that size on rollback - prevents duplicate logging - memory usage proportional to number of modified pages - negligible overhead for small transactions - its contents are considered garbage - no useful old value exists - multiple journal files for the same database - missed recovery steps - irreversible corruption - lock acquisition - lock release - undo logging - multi-database coordination - logging protocol - commit protocol - asynchronous transactions - lazy commit