Conversation
Compact() removed the live log and then renamed the compacted copy over it while that copy was still open, discarding both errors. Go opens files on Windows without FILE_SHARE_DELETE, so renaming a file the process still holds open fails with a sharing violation. The rename therefore fails every time on Windows, not just under antivirus interference - and because the original was already removed, every compaction that runs there destroys the store. The error was discarded and s.log.path was updated as if the swap had succeeded, leaving the index describing a log that no longer exists, which surfaces as "failed to read during compaction: EOF" on every subsequent 5-minute tick until the db directory is deleted by hand. Close both logs before the swap, drop the unnecessary os.Remove (os.Rename replaces an existing destination on POSIX and Windows alike), check the rename and fall back to the untouched original on failure, and retry the rename briefly since antivirus and indexers hold transient handles - the Go toolchain retries module-cache renames for the same reason (golang/go#37802). Also adopt an orphaned <name>.db.compact at startup when the log is missing or empty. openAppendLog uses O_CREATE, so without this a store already broken by this bug silently restarts empty and discards the surviving data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Fixes #377.
The problem
Store.Compact()deletes the live log, then renames the compacted copy into its place whilethat copy is still open, discarding the results of both calls:
Go opens files on Windows without
FILE_SHARE_DELETE, so renaming a file the process itself stillholds open fails with a sharing violation. This isn't an occasional antivirus problem — on Windows
the rename fails every time, and because the original was removed one line earlier, every
compaction that runs there destroys the store.
The error is discarded and
s.log.pathis updated anyway, so the index ends up describing a logthat no longer exists. That surfaces as
failed to read during compaction: EOFon everysubsequent 5-minute tick, forever, since nothing revisits the decision.
openAppendLogusesO_CREATE, so a restart silently recreates the log empty and throws away the data stillsitting in the
.compactfile.The fix
os.Remove.os.Renamereplaces an existing destination on POSIX and Windows alike,so removing it first only created a window where a failed rename lost everything.
pre-compaction state, and return the error instead of reporting success.
toolchain retries module-cache renames for exactly this reason (cmd/go: 'Access is denied' when renaming module cache directory [1.13 backport] golang/go#37802).
<name>.db.compactat startup when the log is missing or empty, so storesalready broken by this bug recover their data instead of restarting empty.
Tests
Three new tests in
pkg/storage/hybrid/store_compaction_test.go. Two of them fail onmainonWindows — including the plain success path, which is the point:
All three pass with this change, and the existing suite is unaffected (
go test ./...green).TestCompactKeepsLogWhenSwapFailsforces an unrenameable destination by holding it open; it skipson platforms where that rename legitimately succeeds, so it stays meaningful on Linux/macOS
without being flaky.
Notes for review
being surfaced.
shadow a healthy log. The
.compactfile is fully written and synced before any swap isattempted, and
Compact()holds the store lock, so it is never a partial snapshot.previously orphaned
queue.dbandrepair_runs.dbwere adopted and are now normal.dbfilesagain.
🤖 Generated with Claude Code