fix(store): packed cache hid a writer's own records; EOFError escaped; add fsync (review of #83) - #84
Merged
Merged
Conversation
Post-merge review of #83. - A writer whose disk-clear was skipped by the _packed_stale guard loaded a packed set another process published before its latest writes, so it did not see records it had just written. _load_packed now treats the disk cache as a miss while this process has unpublished writes; the rebuild republishes, which also heals the cache for other readers. - np.load of an empty/truncated .npy raises EOFError, which escaped _load_packed and made every matrix() call raise while that sig stayed. - Data files and the sig tmp are fsynced before os.replace, and the directory after (POSIX), so a power loss cannot leave a durable sig naming data blocks that never reached disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 22, 2026
Member
Author
|
round-3 review: no defect found. Checked: the stale-guard own-write visibility, EOFError on empty or truncated .npy, fsync ordering, the post-publish sweep racing a concurrent writer (the result is a miss, never wrong rows), 6-process put/matrix stress with a row-to-id vector check, and raglab's 49 tests against master. A pre-existing torn read in the per-record stores, one layer below this PR, is filed as #85. |
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.
Post-merge adversarial review of #83. Refs #77.
Defects found
_invalidate_matrixclears the disk cache only on the first write after a publish (the_packed_staleguard that keeps bulk builds cheap). If another process publishes a matrix between two of this process's writes, the second write leaves that set on disk, and this process's nextmatrix()loaded it. Repro: P1put_record(r1); P2matrix()publishes{r1}; P1put_record(r2); P1matrix()returned['r1']. This predates fix: bind the packed matrix to its sig (the #81 torn-read fix missed the matrix) #83 but lives in the same cache. Fix:_load_packedreturns a miss while_packed_staleis set. This process then rebuilds and republishes, which also heals the cache for other readers. The cost is zero extra filesystem calls.EOFErrorescaped_load_packed.np.loadof a zero-length.npyraisesEOFError, which is neitherOSErrornorValueError. So everymatrix()raised for as long as thatsig.jsonstayed, and nothing cleared it because the save path is never reached. A zero-length data file behind a durable sig is exactly what a power loss leaves when there is no fsync (see 3). Now it counts as a miss.os.replacecan reach the disk before the data blocks of the files it names. Each data file and the sig tmp are now fsynced beforeos.replace, and the directory after it (POSIX only; on Windows the directory fsync is skipped).Checked and holding
os.replaceisrename(2)on Linux and macOS andMoveFileEx(REPLACE_EXISTING)on Windows. On Windows it can fail withPermissionErrorwhile another process hassig.jsonopen. That lands inexcept OSError, which clears the cache, so the result is a miss and never wrong rows.FileNotFoundError, which is a miss. A reader that already has the matrix mmapped keeps the inode on POSIX. On Windows the unlink fails and a later sweep removes the file.sig-*.tmpare swept on the next save or clear. Mixed-version use (an olderirclearing) can leave generation files behind until a newerirsaves. Minor.raglabdoes reach this path.ir_sourcesgoes throughir.as_retriever(name)toCorpusStore.local, which setspacked_dir. raglab's tests pass against this branch (49 passed).Tests
Three regression tests. All fail on master and pass here: own-write visibility, empty/truncated matrix counts as a miss, and fsync happens before publish. Locally: 569 passed, 8 skipped (pytest + doctests, py3.12). Ruff is clean on the changed files;
tests/test_select.pyhas 2 lint findings that were already on master.Not addressed: the cross-process staleness race stays open under #77. A process that only writes and never reads can still leave another process's older build published.
Self-reviewed only (this reviewer was told not to spawn a sub-agent).
🤖 Generated with Claude Code