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
28 changes: 26 additions & 2 deletions find_replace.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type findReplace struct {
find string
replace string

workers chan struct{}

// errs accumulates non-fatal errors that occurred during a walk. The
// walker logs each error at the point of failure (preserving the
// operator-visible UX) and appends it here so main can surface a
Expand Down Expand Up @@ -69,6 +71,18 @@ func main() {
os.Exit(run(os.Args, os.Stderr))
}

func (fr *findReplace) acquireWorker() {
if fr.workers != nil {
fr.workers <- struct{}{}
}
}

func (fr *findReplace) releaseWorker() {
if fr.workers != nil {
<-fr.workers
}
}

// run is the testable body of main. It returns the process exit code: 0 on
// clean success, 1 if argument parsing failed or any traversal error was
// recorded. Output documented in the README (Renaming/Rewriting lines) still
Expand All @@ -82,7 +96,10 @@ func run(args []string, stderr io.Writer) int {
return 1
}

fr := findReplace{find: args[1], replace: args[2]}
fr := findReplace{
find: args[1], replace: args[2],
workers: make(chan struct{}, workerLimit()),
}

// Recursively explore the hierarchy depth first, rewrite files as needed,
// and rename files last (after we don't have to revisit them).
Expand Down Expand Up @@ -112,6 +129,9 @@ func run(args []string, stderr io.Writer) int {
// A failure to read the directory itself is recorded and returned to the
// caller, but does not abort the rest of the walk in any other subtree.
func (fr *findReplace) WalkDir(f *File) {
if fr.workers == nil {
fr.workers = make(chan struct{}, workerLimit())
}
var wg sync.WaitGroup

// List the files in this directory.
Expand All @@ -131,9 +151,13 @@ func (fr *findReplace) WalkDir(f *File) {
fr.errs.add(err)
continue
}
fr.acquireWorker()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid holding worker slots across recursive walks

When the tree contains a path deeper than workerLimit() (or a directory with workerLimit() sibling directories that each need to descend), every directory goroutine keeps its slot until HandleFile returns, then WalkDir tries to acquire another slot for the child before any ancestor can release. At that point all slots can be held by goroutines blocked in recursive WalkDir, so the traversal hangs indefinitely instead of finishing.

Useful? React with 👍 / 👎.

wg.Add(1)
go func() {
defer wg.Done()
defer func() {
fr.releaseWorker()
wg.Done()
}()
if err := fr.HandleFile(childFile); err != nil {
log.Print(err)
fr.errs.add(err)
Expand Down
14 changes: 14 additions & 0 deletions walk_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package main

import "runtime"

func workerLimit() int {
n := runtime.GOMAXPROCS(0) * 2
if n < 4 {
return 4
}
if n > 32 {
return 32
}
return n
}
10 changes: 10 additions & 0 deletions walk_pool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import "testing"

func TestWorkerLimit(t *testing.T) {
n := workerLimit()
if n < 4 || n > 32 {
t.Fatalf("workerLimit() = %d; want in [4, 32]", n)
}
}