From 95b3818da8fcce06f7716bb12f7c63504d9731fb Mon Sep 17 00:00:00 2001 From: tw93 Date: Sat, 7 Feb 2026 11:01:00 +0800 Subject: [PATCH] fix(analyze): fix scan deadlock with non-blocking fallback and add regression test (#419) --- cmd/analyze/analyze_test.go | 38 +++++++++++++++++++++++++++++++++++++ cmd/analyze/scanner.go | 20 ++++++++++++------- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/cmd/analyze/analyze_test.go b/cmd/analyze/analyze_test.go index 243b959..6618c01 100644 --- a/cmd/analyze/analyze_test.go +++ b/cmd/analyze/analyze_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/gob" + "fmt" "os" "path/filepath" "strings" @@ -448,3 +449,40 @@ func TestScanPathPermissionError(t *testing.T) { t.Logf("unexpected error type: %v", err) } } + +func TestCalculateDirSizeFastHighFanoutCompletes(t *testing.T) { + root := t.TempDir() + + // Reproduce high fan-out nested directory pattern that previously risked semaphore deadlock. + const fanout = 256 + for i := 0; i < fanout; i++ { + nested := filepath.Join(root, fmt.Sprintf("dir-%03d", i), "nested") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatalf("create nested dir: %v", err) + } + if err := os.WriteFile(filepath.Join(nested, "data.bin"), []byte("x"), 0o644); err != nil { + t.Fatalf("write nested file: %v", err) + } + } + + var files, dirs, bytes int64 + current := &atomic.Value{} + current.Store("") + + done := make(chan int64, 1) + go func() { + done <- calculateDirSizeFast(root, &files, &dirs, &bytes, current) + }() + + select { + case total := <-done: + if total <= 0 { + t.Fatalf("expected positive total size, got %d", total) + } + if got := atomic.LoadInt64(&files); got < fanout { + t.Fatalf("expected at least %d files scanned, got %d", fanout, got) + } + case <-time.After(5 * time.Second): + t.Fatalf("calculateDirSizeFast did not complete under high fan-out") + } +} diff --git a/cmd/analyze/scanner.go b/cmd/analyze/scanner.go index 9ab71ad..d7f5e97 100644 --- a/cmd/analyze/scanner.go +++ b/cmd/analyze/scanner.go @@ -351,14 +351,20 @@ func calculateDirSizeFast(root string, filesScanned, dirsScanned, bytesScanned * for _, entry := range entries { if entry.IsDir() { subDir := filepath.Join(dirPath, entry.Name()) - sem <- struct{}{} - wg.Add(1) - go func(p string) { - defer wg.Done() - defer func() { <-sem }() - walk(p) - }(subDir) atomic.AddInt64(dirsScanned, 1) + + select { + case sem <- struct{}{}: + wg.Add(1) + go func(p string) { + defer wg.Done() + defer func() { <-sem }() + walk(p) + }(subDir) + default: + // Fallback to synchronous traversal to avoid semaphore deadlock under high fan-out. + walk(subDir) + } } else { info, err := entry.Info() if err == nil {