Fix read-only open during checkpoint from reading inconsistent state#615
Fix read-only open during checkpoint from reading inconsistent state#615ericyuanhui wants to merge 1 commit into
Conversation
Signed-off-by: ericyuanhui <285521263@qq.com>
|
Note that DuckDB and SQLite take different approaches here: https://claude.ai/share/9ce5d5a8-bf34-46cb-87e7-9f1636491c4f |
ladybug is same like SQlite right? |
|
Do you use a filesystem that supports snapshots? From a maintainability and complexity perspective, I prefer a solution like this: https://dev.to/shiviyer/step-by-step-implementation-filesystem-snapshots-in-postgresql-39ci The SQLite method works even if the reader doesn't have write permissions to the dir where the database resides. Also our internals are different from SQLite and the development methodology is not as tightly controlled. That requires us to operate with simpler abstractions. |
What I'm asking is: is there anything wrong with my approach to this modification and implementation? |
|
Let me come back to this after the 0.18.x release. Requires more careful consideration. |
new version is release. If we can discuss this PR? |
|
Still looking to get 0.18.1 out with some bug fixes before letting in big PRs. But this is a good time to discuss the main ideas behind the PR and why it works. |
|
None of these are an objection to the PR. Obviously what it's doing is useful based on feedback from @M0nkeyFl0wer and possibly others.
I would argue that network protocol implementation or a distributed consensus should exist in another library. Someone could then combine all 3 libraries to make a fully functional database.
Certainly it provides value. But it also adds complexity. Which one is greater?
I think the answer is yes, more on it in my next comment below.
Many filesystems implement zero-copy snapshots. So one can do: or These will keep the complexity in the filesystem and/or orchestration between the fs snapshot and DB blocking of further writes. The complexity containment is the #1 concern I have around this PR. |
SQLite uses POSIX advisory byte-range locks on the database file itself with a five-state lock state machine:
The PR's intent.lock / apply.lock pair is conceptually the same two-phase idea as RESERVED → PENDING → EXCLUSIVE, just expressed as two separate files instead of byte-range offsets inside one file. Both designs explicitly model "first announce intent, then escalate to the apply/mutate phase." SQLite's lock state machine is documented in docs/lockvar.tex and src/pager.c in the SQLite source.
The "intention lock" terminology comes from Jim Gray's paper "Granularity of Locks and Degrees of Consistency in a Shared Database" (1975/1976), which defined the hierarchical lock modes that every textbook DBMS teaches:
The whole point of an "intention" lock is exactly what this PR does: signal at a coarse level that you intend to take a
The ARIES recovery algorithm (used in IBM Db2, PostgreSQL, SQL Server, and many others) has an explicit "prepare" phase (write intent record to log, acquire locks, flush) and a separate "commit" phase (write commit record, release locks). The "intent lock" terminology shows up in the ARIES/2PL implementations of these systems. The PR's "intent" and "apply" naming is reminiscent of that two-phase prepare/apply dance.
pg_basebackup's exclusive mode uses an exclusive lock on the database plus a backup_label file. A reader doing PITR/recovery detects the backup_label and refuses to start up (you have to remove it or use the non-exclusive mode). This is the same defensive "fail closed if intermediate state is on disk" idea the PR implements, but at the file-label level rather than a separate lock file. |
Races the PR does not cover
|
Data point from the deployment that produced the #642 repro: no. Plain ext4 on What I actually deployed is the userland equivalent you're describing: the
That last point is my honest answer to the value-vs-complexity question. The On the "does this belong in the engine or outside" framing: SQLite's answer to The #642 repro harness validates any revision of this fix in about a minute Empirical check of race #1 (long-lived reader vs. writer checkpoints)Since race #1 was flagged as the biggest gap, I tested it on stock 0.18.0 Result: it did not reproduce — across ~1,700 writer checkpoints and The named mechanism also appears unreachable in 0.18.0: the data file never Two side observations from the same runs, possibly worth their own issues: If useful, I can file the race-#1 lifetime question as a separate tracking |
Agreed. I found other arguments you make in the comment also compelling. This fixes a real problem. As long as we don't sell it as a complete solution for reader-writer synchronization, we're good to go. Yes, please open an issue for tracking race #1. |
|
I'll merge this after the 0.18.1 release is made. |
Summary
This change fixes a bug where opening the database in read-only mode could race with an in-progress checkpoint and end up reading inconsistent on-disk state. In that situation, recovery could observe intermediate checkpoint artifacts and continue loading from partially updated metadata, which could lead to crashes such as segmentation faults.
To address that, the fix makes checkpoint progress explicit and blocks read-only recovery from entering that unsafe window. It adds coordination around checkpoint execution so read-only opens can detect that a checkpoint is underway and fail fast instead of trying to recover from an intermediate state. It also adds defensive validation when reading checkpoint metadata so invalid or out-of-range page information is rejected with a controlled error rather than being used blindly. In addition, it cleans up a related resource-handling issue in file locking so failed lock attempts do not leak OS handles.
In short, the change turns a potentially unsafe concurrent recovery path into a safe, well-defined failure mode, and adds extra validation to prevent corrupted or partial checkpoint state from causing low-level crashes.
What Changed
Added two checkpoint lock files:*.checkpoint.intent.lock
*.checkpoint.apply.lock
Added checkpoint lock acquisition/release in Checkpointeracquire at checkpoint start
release on cleanup and rollback
Added eager creation of checkpoint lock files for non-read-only persistent databases
Added read-only recovery protection in WALReplayerif a checkpoint is in progress, opening in read-only mode now throws a RuntimeException
read-only recovery no longer proceeds through shadow file / checkpoint WAL intermediate states
Added defensive validation when reading checkpoint headersvalidate catalog page range
validate metadata page range
validate dataFileNumPages against actual file size
Fixed file descriptor / handle leaks when file locking fails in LocalFileSystem
Updated WAL testsReadOnlyRecoveryWithShadowFile now expects failure in read-only mode
added ReadOnlyRecoveryWithCheckpointWAL