Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/core/cycle_detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ package core
import (
"fmt"
"strings"
"sync/atomic"
)

type cycleDetector struct {
graph *BuildGraph
stopped bool
stopped atomic.Bool
}

// Check runs a single check of the build graph to see if any cycles can be detected.
// If it finds one an errCycle is returned.
func (c *cycleDetector) Check() *errCycle {
if c.stopped {
if c.stopped.Load() {
return nil
}
log.Debug("Running cycle detection...")
Expand All @@ -27,7 +28,7 @@ func (c *cycleDetector) Check() *errCycle {
// cycle is complete or not (if not the caller will need to add its node to it as well).
var visit func(target *BuildTarget) ([]*BuildTarget, bool)
visit = func(target *BuildTarget) ([]*BuildTarget, bool) {
if c.stopped {
if c.stopped.Load() {
return nil, false
} else if _, present := complete[target]; present {
return nil, false
Expand All @@ -49,7 +50,7 @@ func (c *cycleDetector) Check() *errCycle {
}

for _, target := range c.graph.AllTargets() {
if c.stopped {
if c.stopped.Load() {
log.Debug("Cycle detection terminated")
return nil
}
Expand All @@ -66,7 +67,7 @@ func (c *cycleDetector) Check() *errCycle {

// Stop stops any existing run of the cycle detector.
func (c *cycleDetector) Stop() {
c.stopped = true
c.stopped.Store(true)
}

// An errCycle is emitted when a graph cycle is detected.
Expand Down
19 changes: 19 additions & 0 deletions src/core/cycle_detector_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package core

import (
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -71,4 +72,22 @@ func TestCycleDetector(t *testing.T) {
log.Warning("%s", err)
assert.Equal(t, []*BuildTarget{g, e, f}, err.Cycle)
})

// This is a regression test for a race with `go state.checkForCycles()`
// in `src/core/state.go`. The main goroutine can call Stop while the
// async check is in flight.
t.Run("StopDuringCheck", func(t *testing.T) {
state := NewDefaultBuildState()
for i := 0; i < 100; i++ {
state.Graph.AddTarget(NewBuildTarget(ParseBuildLabel(fmt.Sprintf("//src:t%d", i), "")))
}
detector := cycleDetector{graph: state.Graph}
done := make(chan struct{})
go func() {
defer close(done)
assert.Nil(t, detector.Check())
}()
detector.Stop()
<-done
})
}
Loading