Repository navigation
fix(droplets): synchronize concurrent appends when creating multiple droplets - #2016
Open
aniruddhaadak80 wants to merge 1 commit into
Open
aniruddhaadak80 wants to merge 1 commit into
aniruddhaadak80 wants to merge 1 commit into
Conversation
RunDropletCreate fans out one goroutine per requested name, and each one appended to the shared createdList slice with no synchronization. Only wg and errs were safe: concurrent appends on the same slice header race, and a lost append silently drops a droplet the API already created, so the user is never told about a droplet that is really running. Guard the append with a mutex, which is how the rest of commands/ protects shared state. TestDropletCreateMultipleNames parks every Create call until all goroutines are in flight and then releases them together, so the race detector fires on the unsynchronized append.
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.
Summary
doctl compute droplet createwith multiple names has a data race. Each requested name gets its own goroutine, and every one of them appends to the samecreatedListslice with no synchronization, so droplets get silently dropped from the result table.This is a self-identified defect found reading the source, not a reported issue.
Problem
RunDropletCreatefans out one goroutine per name.wganderrsare synchronized;createdListis not. Concurrentappendon a shared slice header is undefined behaviour — entries are lost, or the backing array is corrupted.Expected: all requested droplets appear in the printed table.
Actual: fewer rows than droplets created, nondeterministically.
Reproduces on: current
main@9248d399.Only affects the multi-name form, so it has gone unnoticed: the single-name path never has two goroutines racing.
Root cause
commands/droplets.go, inside the per-name goroutine:createdListis declared once outside the loop and captured by every goroutine.Changes
commands/droplets.go— addedcreatedListMu sync.Mutexand lock/unlock around the append. Six added lines, nothing removed. I chose the mutex over a results channel because the surrounding function already usessync.WaitGroupand a buffered error channel for its other shared state;wganderrsare untouched.commands/droplets_test.go—TestDropletCreateMultipleNames, a barrier-synchronized multi-name create that makes the race detector fire.Testing
The proof is the race detector, not an assertion on output. Before the fix:
After the fix, clean — zero
DATA RACElines.Reverting only
commands/droplets.goand keeping the test gives 3WARNING: DATA RACEreports, all atdroplets.go:351; re-applying the fix gives PASS.Please read this before reviewing
CI does not run
-race.make test_unitis plaingo test -mod=vendor ..., so this new test will be green in CI and the race will not be visible there. The non-race assertion — all 16 names present in the JSON output — is inherently probabilistic, since a lostappendis timing-dependent and may not reproduce on a given run. The barrier in the test is what makes the-racesignal deterministic; if you'd prefer CI to catch this class of bug generally, adding-racetotest_unitwould be a separate, larger change.Pre-existing and unrelated, both reproduced at base with this branch reverted:
pkg/extractsymlink tests fail on Windows (needs a privilege this account lacks), andTestMaybePrintAgentPublicPreviewTermsNoticeFirstSessionOnlyhangs atcommands/agents_help_test.go:138, which is the reason for-skip.I also checked for a second, separate capture bug:
go.moddeclaresgo 1.26.0, so per-iteration loop variables apply, anddcris declared inside the loop body regardless. The closure capture is already correct — no second bug exists.Compatibility / risk
None. One uncontended mutex lock per requested name. No behaviour, signature, or API change;
wg/errssemantics unchanged. Output ordering across droplets remains nondeterministic exactly as before.Screenshots / logs
Not applicable — no output-format change.
git commit -s) where DCO is required.