Skip to content

fix(cache): Write files atomically and mutex cache reads - #50

Merged
nikolaik merged 2 commits into
mainfrom
nikolaik/ashamed-waves
Aug 18, 2026
Merged

fix(cache): Write files atomically and mutex cache reads #50
nikolaik merged 2 commits into
mainfrom
nikolaik/ashamed-waves

Conversation

@nikolaik

@nikolaik nikolaik commented Aug 10, 2026

Copy link
Copy Markdown

Changes

  1. Atomic writes (fixes read-while-writing). FileStorage.Create now returns a new CacheWriter interface (io.Writer + Commit() + Close()). StoreInPath.Create writes to a temp file (os.CreateTemp in the same dir) and only os.Renames it into place on Commit(). Since rename is atomic on the same filesystem, readers see either the complete old file or the complete new file — never a partial one. Closing without committing discards the temp file, so failed/partial downloads leave no garbage.

  2. Ref-counted keyed mutex (fixes write/write + dedups downloads). Cache gained a map[string]*keyLock guarded by a sync.Mutex. lockKey(filename) serializes work per-artifact and cleans up the entry once no goroutine holds or waits on it. On a cache miss, only one goroutine downloads; others block, then serve the freshly cached file.

  3. Rewrote ProxyDownload into: verify RepoHead → serve-from-cache fast path → acquire per-file lock → re-check cache → download-to-cache → serve → fall back to direct proxy if caching is unavailable. RepoHead now runs up front on every path, so cached content is never served without an access check.

Prompt

Do we need a mutex or similar to protect against two go routines writing to the same file and/or reading from a file while writing in ProxyDownload?

@nikolaik
nikolaik requested a review from a team as a code owner August 10, 2026 13:52
@nikolaik
nikolaik requested a balanced review from Copilot August 10, 2026 13:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds atomic cache-file publication and per-artifact synchronization to prevent partial reads and duplicate downloads.

Changes:

  • Introduces atomic temporary-file writes with explicit commit.
  • Adds keyed locking and cache-miss rechecks.
  • Updates cache test doubles for the new writer interface.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
services/modules/cache.go Implements atomic writes and synchronized downloads.
services/modules/cache_test.go Updates mocks for commit-based cache writes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread services/modules/cache.go
Comment thread services/modules/cache.go
Comment thread services/modules/cache_test.go Outdated
Comment thread services/modules/cache.go
Changes:

1. **Atomic writes (fixes read-while-writing).** `FileStorage.Create` now returns a new `CacheWriter` interface (`io.Writer` + `Commit()` + `Close()`). `StoreInPath.Create` writes to a temp file (`os.CreateTemp` in the same dir) and only `os.Rename`s it into place on `Commit()`. Since rename is atomic on the same filesystem, readers see either the complete old file or the complete new file — never a partial one. Closing without committing discards the temp file, so failed/partial downloads leave no garbage.

2. **Ref-counted keyed mutex (fixes write/write + dedups downloads).** `Cache` gained a `map[string]*keyLock` guarded by a `sync.Mutex`. `lockKey(filename)` serializes work per-artifact and cleans up the entry once no goroutine holds or waits on it. On a cache miss, only one goroutine downloads; others block, then serve the freshly cached file.

3. **Rewrote `ProxyDownload`** into: verify `RepoHead` → serve-from-cache fast path → acquire per-file lock → re-check cache → download-to-cache → serve → fall back to direct proxy if caching is unavailable. `RepoHead` now runs up front on every path, so cached content is never served without an access check.

Prompt:

> Do we need a mutex or similar to protect against two go routines writing to the same file and/or reading from a file while writing in `ProxyDownload`?
@nikolaik
nikolaik force-pushed the nikolaik/ashamed-waves branch from 6559d3e to 5bd6901 Compare August 18, 2026 07:44
Comment thread services/modules/cache.go
@nikolaik
nikolaik merged commit 533e740 into main Aug 18, 2026
3 checks passed
@nikolaik
nikolaik deleted the nikolaik/ashamed-waves branch August 18, 2026 09:29
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.

3 participants