8000 fix(storage): queries wait for a checkpoint instead of reading it half-written (#558) by adityamukho · Pull Request #560 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

fix(storage): queries wait for a checkpoint instead of reading it half-written (#558) - #560

Merged
adityamukho merged 2 commits into
mainfrom
fix/558-checkpoint-query
Oct 11, 2026
Merged

adityamukho merged 2 commits into
mainfrom
fix/558-checkpoint-query

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Refs #558 (data-integrity: stays open until a release ships the fix). v2.x only; v3's copy-on-write checkpoints are not affected.

Stacked on #559 (the #554/#555 backport, which adds tests/snapshot_atomicity_test.rs). The change here is the last commit, 5d0304c.

Bug

save() writes the new fact pages at 1 + old_fact_page_count, over the pages of the index that queries on other threads read through the published CommittedIndexReader. It then published the new readers and cleared the pending facts with three separately locked calls. A query during a checkpoint could miss committed facts (1 seen of 43) or see part of a transaction. This includes the automatic checkpoint every wal_checkpoint_threshold WAL entries and the one at close.

Fix

FactStorage::lock_for_checkpoint returns a CheckpointGuard on the store's write lock. save():

  • takes the guard before the backend lock, the same order queries use (store read lock, then backend);
  • reads the pending facts through the guard;
  • installs both readers and clears pending in one step (CheckpointGuard::publish).

Queries now wait for the whole checkpoint, including its fsyncs; commits were already serialized with it by the write lock. get_pending_facts and post_checkpoint_clear are now test-only.

Tests

checkpoint_never_hides_committed_facts: a writer commits a pair of facts and checkpoints, in a loop. A reader checks that no entity has half a pair and that it never sees fewer entities than were committed before the query began.

  • Without the fix it fails in all 3 runs within 30 ms; with it, it passes for the full 3 s.
  • 1241 passing, 9 ignored. fmt and clippy clean.

Codex review

No deadlock and no misplaced guard found. One point is out of scope: if save() fails after its first page write, the old readers stay published over overwritten pages, so later queries on that handle can be wrong. That happens without any concurrency. It is the in-process form of the known v2.x issue #374 (in #421), and v3 handles it through #390's refuse-after-failed-checkpoint, which has no v2.x backport.

🤖 Generated with Claude Code

https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h

adityamukho and others added 2 commits October 11, 2026 23:37
Backport of the v3 fixes (PRs #556, #557) to v2.x.

#554: commits added a transaction's facts to FactStorage one load_fact
call at a time, each under its own write lock, so a query on another
thread could see part of the transaction. FactStorage::load_facts
appends a batch under one lock; execute, WriteTransaction::commit, WAL
replay and BrowserDb's commit use it.

#555: selective_fact_fetch read each entity/attribute lookup under its
own read lock, so a commit between two lookups was seen by some
patterns and not others. The three lookup bodies become functions over
&FactData and FactStorage::get_facts_for_lookups runs them all under one
read guard. The old methods remain for tests only.

The file-backed #555 reproducer disables checkpoints: on v2.x a query
that runs during a checkpoint can lose committed facts, a separate bug.

Refs #554, #555

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h
…f-written (#558)

save() appends the new fact pages at 1 + old_fact_page_count, over the
pages of the index that queries on other threads read through the
published CommittedIndexReader, and then published the new readers and
cleared the pending facts with three separately locked calls. A query
during a checkpoint could miss committed facts or see part of a
transaction.

FactStorage::lock_for_checkpoint returns a guard on the store's write
lock. save() takes it before the backend lock (the order queries use:
store read lock, then backend), reads the pending facts through it, and
publishes both readers and clears pending in one step
(CheckpointGuard::publish). get_pending_facts and post_checkpoint_clear
are now test-only.

v3 is not affected (copy-on-write checkpoints).

Refs #558

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wXxZguv7PgKQjMMH4yJ5h
@codecov
codecov Bot commented Oct 11, 2026
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/graph/storage.rs 87.71% 14 Missing ⚠️

📢 Thoughts on this report? Let us know!

@adityamukho
adityamukho merged commit 72d847e into main Oct 11, 2026
22 checks passed
@adityamukho
adityamukho deleted the fix/558-checkpoint-query branch October 11, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0