Repository navigation
Conversation
A checkpoint taken while a read-only transaction is open keeps its CHECKPOINT record in the active WAL. Recovery treated a shadow file left next to such a WAL as a checkpoint in progress: it replayed only the records before the first CHECKPOINT (applying them twice and dropping every later commit), replayed the next checkpoint's unfinished shadow, or required partition shadows that cleanup had already removed. - A whole transaction after the last CHECKPOINT record proves that checkpoint completed. Recovery then skips through the record and removes the leftover shadow, in the active and the frozen WAL. Other records after a CHECKPOINT keep the prefix replay. - The pinned-checkpoint marker now means that every CHECKPOINT record in the active WAL is applied: WAL::logAndFlushCheckpoint removes it before logging a record, and pinned cleanup writes it (content 2) before removing shadows. With it, a WAL ending in a CHECKPOINT discards leftover main and partition shadows instead of replaying or requiring them. - A WAL with nothing committed after its last CHECKPOINT is recovered as one ending in it, so it is retired instead of kept. - A marker from an older build (content 1) only proves that a missing shadow was applied. Named graphs are not covered: pinned checkpoints keep their own WALs too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 24, 2026
…read-only refusal Only a COMMIT without its BEGIN keeps the prefix replay; records that end in no COMMIT are a torn tail of the last CHECKPOINT. A read-only open refuses a WAL that ends in a CHECKPOINT record, now also one followed by a torn commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he CHECKPOINT A kill after WAL::logAndFlushCheckpoint removes the pinned-checkpoint marker and before it writes the CHECKPOINT record leaves the WAL ending in the earlier, applied CHECKPOINT, no marker, and the next checkpoint's complete shadow. Three tests cover it: nothing committed in between, commits in between, and a partitioned table whose shadows the dying checkpoint never wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This work was produced with the help of language models.
Part of #1304.
After a
CHECKPOINTtaken while another connection had a read-only transaction open, a crash could lose commits or block reopening. With a commit after it and a crash during the next checkpoint, reopening applied the earlier transactions twice and dropped every later one (Found duplicated primary key value 1, …,Catalog exception: P already exists in catalog., or 2 rows of 100 in #1304's failed-checkpoint repro). Without a commit in between, the next checkpoint's unfinished shadow failed the reopen withCannot replay shadow file db.shadow: file is too small., as did a crash while cleanup removed the applied shadow; one just after it gaveshadow file db.shadow is missing with no pinned-checkpoint marker.. A partitioned table failed withpartition shadow file db.L_p0.shadow is missing.after the pinned checkpoint alone.Fix. Such a checkpoint keeps its
CHECKPOINTrecord in the active WAL, and recovery read any shadow beside it as a checkpoint in progress. A transaction committed after the record proves the checkpoint completed: the write gate is held until cleanup ends, and a failure after the record poisons the WAL. Recovery now skips through the lastCHECKPOINTwhen a whole transaction follows it, in the active and the frozen WAL, and removes the leftover shadow; a loneCOMMITthere, which the engine never writes, keeps the prefix replay, and records that end in noCOMMITare a torn tail of theCHECKPOINT. A WAL with nothing committed after its lastCHECKPOINTis retired instead of kept.The
db.pinned.tmpmarker now means that everyCHECKPOINTrecord in the active WAL is applied:WAL::logAndFlushCheckpointremoves it before logging one, and pinned cleanup writes it before removing any shadow. When the WAL ends in aCHECKPOINT, recovery with the marker discards leftover main and partition shadows, and without it replays a remaining shadow as before.docs/checkpoint_recovery.mdgets a section on these rules.Read-only opens. Recovering a WAL that ends in a
CHECKPOINTwrites, so a read-only open refuses it withCannot open database in read-only mode while checkpoint recovery is pending, as onmain. Counting a torn commit after a pinnedCHECKPOINTas part of that ending extends the refusal to a crash in the middle of a commit, wheremainreplayed the records before the firstCHECKPOINTa second time; opening read-write once recovers such a database. The refusal could be relaxed: when the marker says the checkpoint is applied, or no shadow is left, the data file already holds it and no transaction follows, so a read-only open could load the checkpoint and leave the WAL, shadow and marker files for the next read-write open. That takes a read-only branch inreplayPinnedActiveCheckpoint, which now truncates the WAL and removes those files, about ten lines and a test, and would also open a clean trailingCHECKPOINTread-only in those cases, whichmainrefuses. It is left out to keep the change to recovery small; it can follow if read-only opens of crashed databases matter.Compatibility. The marker now holds
2. An older build's1only proves that a missing shadow was applied, since older builds kept it while logging the next record; older builds only check that the marker exists.Not fixed here. A database with a named graph (
CREATE GRAPH) still fails to reopen after such a crash (graph shadow file db.g.shadow is missing.), since recovery still requires the graph's shadow that cleanup removed. It needs the partition rule plus handling for a graph WAL left by a standalone session, and follows in #1379.Tests.
PinnedCheckpointCrashTest(test/transaction/checkpoint_test.cpp) has 13 crash-and-reopen tests; 8 fail onmainwith the errors above. They leave a later checkpoint's shadow after commits (also in a frozen WAL, restored from a mid-checkpoint copy of the files), a cut-short or empty shadow, a partitioned table, and a loneBEGINafter theCHECKPOINT. A newbeforeShadowRemovalhook checks that the marker is durable first. Two tests crash between logging a record and applying its shadow, under an earlier and a legacy marker, and fail when the marker removal or the content check is taken out; the rest keep whatmaindoes with legacy and missing markers. Three more kill the next checkpoint after it removed the marker and before it logged its record: with nothing committed in between, with later commits, and with a partitioned table whose shadows that checkpoint never wrote; each reopens with every committed row. Full ctest: 3197/3197; storage-compat suite, every release: same results asmain.