From 35f249e6064a9e83343e82be1510d37681837f48 Mon Sep 17 00:00:00 2001 From: Jack Marsh Date: Sun, 16 Aug 2026 22:32:37 +0100 Subject: [PATCH] Fix lost-wakeup race on packageWaits in WaitForPackage Concurrent callers waiting on the same unparsed package could both pass the Get(key) == nil check and both Set a fresh channel; the second Set overwrote the first, and LogParseResult closes only the channel currently in the map, so the first waiter slept forever and the build wedged with "N tasks left, 0 workers busy". Use AddOrGet, as pendingPackages and pendingTargets already do. --- src/core/state.go | 6 ++++-- src/core/state_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/core/state.go b/src/core/state.go index 18a03d70ab..2a9ca6fb0a 100644 --- a/src/core/state.go +++ b/src/core/state.go @@ -884,14 +884,16 @@ func (state *BuildState) WaitForPackage(l, dependent BuildLabel, mode ParseMode) } // If something has already queued the package to be parsed, wait for them - if ch := state.progress.packageWaits.Get(key); ch != nil { + // (atomically: a racing Get-then-Set here can orphan the first caller's channel) + if ch, inserted := state.progress.packageWaits.AddOrGet(key, func() chan struct{} { + return make(chan struct{}) + }); !inserted { waitOnChan(ch, "Still waiting for package wait in WaitForPackage(%v, %v, %v)", l, dependent, mode) return state.Graph.PackageByLabel(l) } // Otherwise queue the target for parse and recurse state.addPendingParse(l, dependent, mode) - state.progress.packageWaits.Set(key, make(chan struct{})) return state.WaitForPackage(l, dependent, mode) } diff --git a/src/core/state_test.go b/src/core/state_test.go index 010988a24e..1500de9971 100644 --- a/src/core/state_test.go +++ b/src/core/state_test.go @@ -2,7 +2,9 @@ package core import ( "strings" + "sync" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -167,3 +169,39 @@ func TestCopyPlugin(t *testing.T) { assert.NotEqual(t, plugin.ExtraValues["foo"], newPlugin.ExtraValues["foo"]) } + +func TestWaitForPackageConcurrent(t *testing.T) { + // Regression test for a lost-wakeup race: concurrent callers waiting on + // the same unparsed package could overwrite each other's wait channel in + // packageWaits, so the channel one of them waited on was never closed and + // that caller blocked forever. + dependent := BuildLabel{PackageName: "other", Name: "all"} + for i := 0; i < 200; i++ { + state := NewDefaultBuildState() + label := BuildLabel{PackageName: "pkg", Name: "all"} + const n = 32 + var wg sync.WaitGroup + wg.Add(n) + start := make(chan struct{}) + for j := 0; j < n; j++ { + go func() { + defer wg.Done() + <-start + state.WaitForPackage(label, dependent, ParseModeNormal) + }() + } + close(start) + // Let the waiters register against the unparsed package first, then + // complete the parse the way LogParseResult does for real parses. + time.Sleep(time.Millisecond) + state.Graph.AddPackage(NewPackage("pkg")) + state.LogParseResult(label, PackageParsed, "parsed") + done := make(chan struct{}) + go func() { wg.Wait(); close(done) }() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatalf("iteration %d: a WaitForPackage caller never woke", i) + } + } +}