Skip to content

fix(snapshot): replace unsafe.Pointer CAS with atomic.Pointer[error] - #75

Merged
kavix merged 1 commit into
kavix:mainfrom
charlie-morrison:fix/68-remove-unsafe-pointer
Jul 29, 2026
Merged

fix(snapshot): replace unsafe.Pointer CAS with atomic.Pointer[error]#75
kavix merged 1 commit into
kavix:mainfrom
charlie-morrison:fix/68-remove-unsafe-pointer

Conversation

@charlie-morrison

Copy link
Copy Markdown
Contributor

Closes #68.

RestoreSnapshot recorded the first parallel-deletion error with a raw unsafe.Pointer + atomic.CompareAndSwapPointer. This swaps it for atomic.Pointer[error], which is the same lock-free compare-and-swap with static type safety — the unsafe import is gone from the package.

var firstErr atomic.Pointer[error]
...
firstErr.CompareAndSwap(nil, &rmErr)   // first failure wins, later ones discarded
...
if err := firstErr.Load(); err != nil {
    return *err
}

On the two options in the issue. errgroup would work, but it means adding golang.org/x/sync to a go.mod that currently has two direct dependencies, and a plain sync.Mutex drops the lock-free property the design doc calls out by name. atomic.Pointer[error] is stdlib, needs no new dependency, and keeps the existing CAS semantics exactly — so it seemed like the closest thing to a strict improvement of what was already there. Happy to switch to errgroup or a mutex if you'd rather have it exactly as filed; it's a small change either way.

Behaviour is unchanged — same first-error-wins CAS, same discard of later errors, same short-circuit before the copy phase.

Verification. That error path had no test coverage (every existing RestoreSnapshot test is happy-path), so "no behaviour change" wasn't checkable. This adds TestRestoreSnapshot_returnsRemovalError, which makes a top-level entry un-removable and asserts both that the error surfaces and that phase 2 never ran. I ran it against the original unsafe implementation first, where it also passes — so it's pinning existing behaviour, not describing the new code. It skips under root (uid 0 ignores the directory permissions it relies on) and restores the mode in t.Cleanup so t.TempDir can clean up.

  • go build ./..., go vet ./..., gofmt -l — clean
  • go test -race -count=1 ./... — all packages pass
  • Go 1.26.5, matching your CI

Also updated the Parallel Deletion (Lock-Free CAS) section of CONTRIBUTING.md — it documented the unsafe.Pointer snippet directly, so it would have gone stale on merge.

No behaviour, API, or dependency changes.

RestoreSnapshot recorded the first parallel-deletion error with a raw
unsafe.Pointer and atomic.CompareAndSwapPointer. atomic.Pointer[error]
provides the same lock-free compare-and-swap with static type safety,
removing the unsafe import from the package entirely.

Behaviour is unchanged: the first goroutine to fail still wins the CAS,
later errors are still discarded, and a non-nil result still
short-circuits the restore before the copy phase.

Adds TestRestoreSnapshot_returnsRemovalError, which covers that error
path (previously untested) and passes against both the old and new
implementations. Updates the CONTRIBUTING.md concurrency section, which
documented the unsafe.Pointer snippet.

Closes kavix#68

Signed-off-by: charlie-morrison <charliemorrison2228@gmail.com>
@kavix
kavix merged commit 0b58967 into kavix:main Jul 29, 2026
2 checks passed
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.

Reduce unsafe.Pointer usage in RestoreSnapshot's error handling

2 participants