Repository navigation
Serialize OpenBucket and stop expiration before closing the bucket - #100
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Resolve the lock-order deadlock and make the registration-race test deterministic.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
Fixes OpenBucket reference counting and expiration scheduling during concurrent registration races.
Changes:
- Corrects loser-bucket cleanup without unregistering.
- Synchronizes expiration scheduling.
- Adds concurrent-open regression coverage.
| File | Summary |
|---|---|
bucket.go |
Handles registration-race losers safely. |
bucket_registry_test.go |
Adds concurrent bucket-opening coverage; the race path should be made deterministic. |
bucket_api.go |
Adds expiration scheduling synchronization; a lock-order deadlock must be resolved. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
torcolvin
force-pushed
the
openbucket-register-race
branch
from
September 25, 2026 16:47
55cd851 to
283f14b
Compare
torcolvin
force-pushed
the
openbucket-register-race
branch
from
September 25, 2026 18:04
283f14b to
0ddc53c
Compare
Hold a registry open lock from the cache lookup until registration, so concurrent OpenBucket calls never open the same bucket twice. A CreateNew call that loses now gets fs.ErrExist. Stop the expiry manager before locking the bucket on close, and wait for a running expiration to finish. The expiration no longer holds the expiry manager mutex while it locks the bucket, which removes a lock order inversion with close. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Build with the deadlock tag to replace the package mutexes with go-deadlock mutexes. The job fails on a lock order inversion, even when the test goroutines do not interleave to deadlock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
torcolvin
force-pushed
the
openbucket-register-race
branch
from
September 28, 2026 16:58
0ddc53c to
9471412
Compare
bbrks
approved these changes
Oct 2, 2026
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.


Two
OpenBucketcalls for the same bucket could both miss the cache, so both opened SQLite and both registered. This caused these bugs:Close, which lowered the registry count for the copy that it returned.CreateNewcall that lost the race returned the other call's bucket with no error, instead offs.ErrExist._scheduleExpirationran without the expiry manager lock, while writes (feeds.go:179) scheduled expiration at the same time. This was a data race.Fix:
OpenBucketnow holdsopenLockon the registry fromgetCachedBucketuntil it registers the bucket. Only one call opens a bucket, so no call can lose the race. The bucket schedules expiration before it is registered, and it takes the expiry manager lock to do so.Close and expiration also locked two mutexes in opposite orders, so they could deadlock:
close()lockedbucket.mutex, thenexpManager.mutex.runExpirylockedexpManager.mutex, thenbucket.mutex.Fix:
close()now stops the expiry manager before it locks the bucket.stop()waits for a running expiration to finish.runExpiryholds a separaterunMutex, so no expiration can start afterstop().A new CI job builds with
-tags deadlock. That build usesgo-deadlockmutexes, and the job fails on any lock order inversion, even when the goroutines do not deadlock during the test.Tests:
TestConcurrentOpenBucketchecks that concurrent reopens open the database one time.TestConcurrentCreateNewBucketchecks that oneCreateNewcall succeeds and the others getfs.ErrExist.TestCloseDuringExpirationchecks thatClosewaits for a running expiration. The old code deadlocked in this test.I ran the rosmar tests 200 times with
-race, and 150 times with-race -tags deadlock. I also ran the sync gateway tests with this set of changes, with and without-race.🤖 Generated with Claude Code