Skip to content

Commit e732464

Browse files
Copilotneongreen
andcommitted
Fix flaky "text file busy" error in gopls tests by adding retry logic
Co-authored-by: neongreen <1523306+neongreen@users.noreply.github.com>
1 parent 5aa197f commit e732464

3 files changed

Lines changed: 52 additions & 6 deletions

File tree

dissect/pkg/gopls/add_import.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ func AddImport(goplsPath string, filePath string, importPath string, moduleRoot
2424

2525
// Construct the gopls command
2626
cmd := exec.Command(goplsPath, "execute", "-write", "gopls.add_import", fmt.Sprintf(`{"ImportPath": "%s", "URI": "file://%s"}`, importPath, absFilePath)) //nolint:gosec // G204: intentional gopls execution with validated path
27-
cmd.Dir = moduleRoot // Execute gopls in the Go module root
27+
cmd.Dir = moduleRoot // Execute gopls in the Go module root
2828

2929
slog.Debug("Calling gopls", "command", cmd.String(), "directory", cmd.Dir, "goplsPath", goplsPath)
3030

3131
var stderr bytes.Buffer
3232
cmd.Stderr = &stderr
3333

34-
if err := cmd.Run(); err != nil {
34+
if _, err := runWithTextFileBusyRetry(cmd); err != nil {
3535
return fmt.Errorf("error executing gopls: %w\n%s", err, stderr.String())
3636
}
3737

dissect/pkg/gopls/extract_to_new_file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func ExtractToNewFile(goplsPath string, filePath string, funcName string, module
5555
var stderr bytes.Buffer
5656
cmd.Stderr = &stderr
5757

58-
if err := cmd.Run(); err != nil {
58+
if _, err := runWithTextFileBusyRetry(cmd); err != nil {
5959
return "", fmt.Errorf("error executing gopls: %w\n%s", err, stderr.String())
6060
}
6161

dissect/pkg/gopls/rename.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package gopls
22

33
import (
4+
"bytes"
45
"fmt"
56
"go/ast"
67
"go/parser"
78
"go/token"
89
"log/slog"
910
"os"
1011
"os/exec"
12+
"strings"
13+
"time"
1114
)
1215

1316
// Rename renames a symbol in a Go file using gopls.
@@ -32,12 +35,17 @@ func Rename(goplsPath string, filePath string, oldName string, newName string, m
3235

3336
cmd := exec.Command(goplsPath, "rename", "-w", positionSpec, newName)
3437
cmd.Dir = moduleRoot
35-
output, err := cmd.CombinedOutput()
38+
39+
var combinedOutput bytes.Buffer
40+
cmd.Stdout = &combinedOutput
41+
cmd.Stderr = &combinedOutput
42+
43+
_, err = runWithTextFileBusyRetry(cmd)
3644
if err != nil {
37-
return fmt.Errorf("gopls rename failed: %w\nOutput: %s", err, string(output))
45+
return fmt.Errorf("gopls rename failed: %w\nOutput: %s", err, combinedOutput.String())
3846
}
3947

40-
slog.Debug("Successfully renamed symbol", "old", oldName, "new", newName, "output", string(output), "goplsPath", goplsPath)
48+
slog.Debug("Successfully renamed symbol", "old", oldName, "new", newName, "output", combinedOutput.String(), "goplsPath", goplsPath)
4149
return nil
4250
}
4351

@@ -106,3 +114,41 @@ func findSymbolOffset(filePath string, symbolName string) (int, error) {
106114
position := fset.Position(symbolPos)
107115
return position.Offset, nil
108116
}
117+
118+
// runWithTextFileBusyRetry runs a command and retries if it fails with "text file busy" error.
119+
// This error can occur when a binary was just installed by "go install" and the OS hasn't
120+
// fully released the file handle yet. The function retries up to 3 times with exponential backoff.
121+
// Note: This function uses cmd.Run(), so caller should capture output via cmd.Stdout/Stderr if needed.
122+
func runWithTextFileBusyRetry(cmd *exec.Cmd) ([]byte, error) {
123+
const maxRetries = 3
124+
var lastErr error
125+
126+
for attempt := 0; attempt < maxRetries; attempt++ {
127+
// Create a new command for each attempt since exec.Cmd can only be used once
128+
if attempt > 0 {
129+
newCmd := exec.Command(cmd.Path, cmd.Args[1:]...)
130+
newCmd.Dir = cmd.Dir
131+
newCmd.Env = cmd.Env
132+
newCmd.Stdout = cmd.Stdout
133+
newCmd.Stderr = cmd.Stderr
134+
cmd = newCmd
135+
136+
// Wait before retrying with exponential backoff
137+
waitTime := time.Duration(50*(1<<attempt)) * time.Millisecond
138+
slog.Debug("Retrying command after text file busy error", "attempt", attempt+1, "wait", waitTime)
139+
time.Sleep(waitTime)
140+
}
141+
142+
lastErr = cmd.Run()
143+
if lastErr == nil {
144+
return nil, nil
145+
}
146+
147+
// Check if this is a "text file busy" error
148+
if !strings.Contains(lastErr.Error(), "text file busy") {
149+
return nil, lastErr
150+
}
151+
}
152+
153+
return nil, lastErr
154+
}

0 commit comments

Comments
 (0)