Skip to content

Commit 25a2b98

Browse files
author
Bryan C. Mills
committed
cmd/go: factor the I/O-retry logic out of renameio
Factor the try-on-failure variants are now in the package cmd/go/internal/robustio. Add to them a RemoveAll variant using the same retry loop, and use it to attempt to address the observed flakes in TestLinkXImportPathEscape. Fixes #19491 Updates #25965 Updates #28387 Updates #32188 Change-Id: I9db1a0c7537b8aaadccab1b9eca734595668ba29 Reviewed-on: https://go-review.googlesource.com/c/go/+/181541 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Alex Brainman <[email protected]> Reviewed-by: Jay Conrod <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 55d31e1 commit 25a2b98

File tree

8 files changed

+99
-29
lines changed

8 files changed

+99
-29
lines changed

src/cmd/go/go_test.go

+14-12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"cmd/go/internal/cache"
3131
"cmd/go/internal/cfg"
32+
"cmd/go/internal/robustio"
3233
"cmd/internal/sys"
3334
)
3435

@@ -685,7 +686,7 @@ func (tg *testgoData) creatingTemp(path string) {
685686
if tg.wd != "" && !filepath.IsAbs(path) {
686687
path = filepath.Join(tg.pwd(), path)
687688
}
688-
tg.must(os.RemoveAll(path))
689+
tg.must(robustio.RemoveAll(path))
689690
tg.temps = append(tg.temps, path)
690691
}
691692

@@ -887,7 +888,7 @@ func removeAll(dir string) error {
887888
}
888889
return nil
889890
})
890-
return os.RemoveAll(dir)
891+
return robustio.RemoveAll(dir)
891892
}
892893

893894
// failSSH puts an ssh executable in the PATH that always fails.
@@ -1181,7 +1182,7 @@ func testMove(t *testing.T, vcs, url, base, config string) {
11811182
case "svn":
11821183
// SVN doesn't believe in text files so we can't just edit the config.
11831184
// Check out a different repo into the wrong place.
1184-
tg.must(os.RemoveAll(tg.path("src/code.google.com/p/rsc-svn")))
1185+
tg.must(robustio.RemoveAll(tg.path("src/code.google.com/p/rsc-svn")))
11851186
tg.run("get", "-d", "-u", "code.google.com/p/rsc-svn2/trunk")
11861187
tg.must(os.Rename(tg.path("src/code.google.com/p/rsc-svn2"), tg.path("src/code.google.com/p/rsc-svn")))
11871188
default:
@@ -1693,7 +1694,7 @@ func TestInstalls(t *testing.T) {
16931694
goarch := strings.TrimSpace(tg.getStdout())
16941695
tg.setenv("GOARCH", goarch)
16951696
fixbin := filepath.Join(goroot, "pkg", "tool", goos+"_"+goarch, "fix") + exeSuffix
1696-
tg.must(os.RemoveAll(fixbin))
1697+
tg.must(robustio.RemoveAll(fixbin))
16971698
tg.run("install", "cmd/fix")
16981699
tg.wantExecutable(fixbin, "did not install cmd/fix to $GOROOT/pkg/tool")
16991700
tg.must(os.Remove(fixbin))
@@ -2065,13 +2066,13 @@ func TestDefaultGOPATHGet(t *testing.T) {
20652066
tg.grepStderr("created GOPATH="+regexp.QuoteMeta(tg.path("home/go"))+"; see 'go help gopath'", "did not create GOPATH")
20662067

20672068
// no warning if directory already exists
2068-
tg.must(os.RemoveAll(tg.path("home/go")))
2069+
tg.must(robustio.RemoveAll(tg.path("home/go")))
20692070
tg.tempDir("home/go")
20702071
tg.run("get", "github.com/golang/example/hello")
20712072
tg.grepStderrNot(".", "expected no output on standard error")
20722073

20732074
// error if $HOME/go is a file
2074-
tg.must(os.RemoveAll(tg.path("home/go")))
2075+
tg.must(robustio.RemoveAll(tg.path("home/go")))
20752076
tg.tempFile("home/go", "")
20762077
tg.runFail("get", "github.com/golang/example/hello")
20772078
tg.grepStderr(`mkdir .*[/\\]go: .*(not a directory|cannot find the path)`, "expected error because $HOME/go is a file")
@@ -2872,7 +2873,7 @@ func TestCgoDependsOnSyscall(t *testing.T) {
28722873
files, err := filepath.Glob(filepath.Join(runtime.GOROOT(), "pkg", "*_race"))
28732874
tg.must(err)
28742875
for _, file := range files {
2875-
tg.check(os.RemoveAll(file))
2876+
tg.check(robustio.RemoveAll(file))
28762877
}
28772878
tg.tempFile("src/foo/foo.go", `
28782879
package foo
@@ -3925,10 +3926,10 @@ func TestGoGetDomainRoot(t *testing.T) {
39253926
tg.run("get", "go-get-issue-9357.appspot.com")
39263927
tg.run("get", "-u", "go-get-issue-9357.appspot.com")
39273928

3928-
tg.must(os.RemoveAll(tg.path("src/go-get-issue-9357.appspot.com")))
3929+
tg.must(robustio.RemoveAll(tg.path("src/go-get-issue-9357.appspot.com")))
39293930
tg.run("get", "go-get-issue-9357.appspot.com")
39303931

3931-
tg.must(os.RemoveAll(tg.path("src/go-get-issue-9357.appspot.com")))
3932+
tg.must(robustio.RemoveAll(tg.path("src/go-get-issue-9357.appspot.com")))
39323933
tg.run("get", "-u", "go-get-issue-9357.appspot.com")
39333934
}
39343935

@@ -4513,8 +4514,9 @@ func TestLinkXImportPathEscape(t *testing.T) {
45134514
tg := testgo(t)
45144515
defer tg.cleanup()
45154516
tg.parallel()
4517+
tg.makeTempdir()
45164518
tg.setenv("GOPATH", filepath.Join(tg.pwd(), "testdata"))
4517-
exe := "./linkx" + exeSuffix
4519+
exe := tg.path("linkx" + exeSuffix)
45184520
tg.creatingTemp(exe)
45194521
tg.run("build", "-o", exe, "-ldflags", "-X=my.pkg.Text=linkXworked", "my.pkg/main")
45204522
out, err := exec.Command(exe).CombinedOutput()
@@ -4750,7 +4752,7 @@ func TestExecutableGOROOT(t *testing.T) {
47504752
check(t, symGoTool, newRoot)
47514753
})
47524754

4753-
tg.must(os.RemoveAll(tg.path("new/pkg")))
4755+
tg.must(robustio.RemoveAll(tg.path("new/pkg")))
47544756

47554757
// Binaries built in the new tree should report the
47564758
// new tree when they call runtime.GOROOT.
@@ -5101,7 +5103,7 @@ func TestExecBuildX(t *testing.T) {
51015103
if len(matches) == 0 {
51025104
t.Fatal("no WORK directory")
51035105
}
5104-
tg.must(os.RemoveAll(matches[1]))
5106+
tg.must(robustio.RemoveAll(matches[1]))
51055107
}
51065108

51075109
func TestParallelNumber(t *testing.T) {

src/cmd/go/go_windows_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"path/filepath"
1313
"strings"
1414
"testing"
15+
16+
"cmd/go/internal/robustio"
1517
)
1618

1719
func TestAbsolutePath(t *testing.T) {
@@ -21,7 +23,7 @@ func TestAbsolutePath(t *testing.T) {
2123
if err != nil {
2224
t.Fatal(err)
2325
}
24-
defer os.RemoveAll(tmp)
26+
defer robustio.RemoveAll(tmp)
2527

2628
file := filepath.Join(tmp, "a.go")
2729
err = ioutil.WriteFile(file, []byte{}, 0644)

src/cmd/go/internal/renameio/renameio.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"os"
1313
"path/filepath"
1414
"strconv"
15+
16+
"cmd/go/internal/robustio"
1517
)
1618

1719
const patternSuffix = ".tmp"
@@ -61,7 +63,7 @@ func WriteToFile(filename string, data io.Reader, perm os.FileMode) (err error)
6163
return err
6264
}
6365

64-
return rename(f.Name(), filename)
66+
return robustio.Rename(f.Name(), filename)
6567
}
6668

6769
// ReadFile is like ioutil.ReadFile, but on Windows retries spurious errors that
@@ -74,7 +76,7 @@ func WriteToFile(filename string, data io.Reader, perm os.FileMode) (err error)
7476
// - syscall.ERROR_FILE_NOT_FOUND
7577
// - internal/syscall/windows.ERROR_SHARING_VIOLATION
7678
func ReadFile(filename string) ([]byte, error) {
77-
return readFile(filename)
79+
return robustio.ReadFile(filename)
7880
}
7981

8082
// tempFile creates a new temporary file with given permission bits.

src/cmd/go/internal/renameio/renameio_test.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"syscall"
2020
"testing"
2121
"time"
22+
23+
"cmd/go/internal/robustio"
2224
)
2325

2426
func TestConcurrentReadsAndWrites(t *testing.T) {
@@ -58,7 +60,7 @@ func TestConcurrentReadsAndWrites(t *testing.T) {
5860
chunk := buf[offset*8 : (offset+chunkWords)*8]
5961
if err := WriteFile(path, chunk, 0666); err == nil {
6062
atomic.AddInt64(&writeSuccesses, 1)
61-
} else if isEphemeralError(err) {
63+
} else if robustio.IsEphemeralError(err) {
6264
var (
6365
errno syscall.Errno
6466
dup bool
@@ -74,10 +76,10 @@ func TestConcurrentReadsAndWrites(t *testing.T) {
7476
}
7577

7678
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
77-
data, err := ioutil.ReadFile(path)
79+
data, err := ReadFile(path)
7880
if err == nil {
7981
atomic.AddInt64(&readSuccesses, 1)
80-
} else if isEphemeralError(err) {
82+
} else if robustio.IsEphemeralError(err) {
8183
var (
8284
errno syscall.Errno
8385
dup bool
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2019 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package robustio wraps I/O functions that are prone to failure on Windows,
6+
// transparently retrying errors up to an arbitrary timeout.
7+
//
8+
// Errors are classified heuristically and retries are bounded, so the functions
9+
// in this package do not completely eliminate spurious errors. However, they do
10+
// significantly reduce the rate of failure in practice.
11+
//
12+
// If so, the error will likely wrap one of:
13+
// The functions in this package do not completely eliminate spurious errors,
14+
// but substantially reduce their rate of occurrence in practice.
15+
package robustio
16+
17+
// Rename is like os.Rename, but on Windows retries errors that may occur if the
18+
// file is concurrently read or overwritten.
19+
//
20+
// (See golang.org/issue/31247 and golang.org/issue/32188.)
21+
func Rename(oldpath, newpath string) error {
22+
return rename(oldpath, newpath)
23+
}
24+
25+
// ReadFile is like ioutil.ReadFile, but on Windows retries errors that may
26+
// occur if the file is concurrently replaced.
27+
//
28+
// (See golang.org/issue/31247 and golang.org/issue/32188.)
29+
func ReadFile(filename string) ([]byte, error) {
30+
return readFile(filename)
31+
}
32+
33+
// RemoveAll is like os.RemoveAll, but on Windows retries errors that may occur
34+
// if an executable file in the directory has recently been executed.
35+
//
36+
// (See golang.org/issue/19491.)
37+
func RemoveAll(path string) error {
38+
return removeAll(path)
39+
}
40+
41+
// IsEphemeralError reports whether err is one of the errors that the functions
42+
// in this package attempt to mitigate.
43+
//
44+
// Errors considered ephemeral include:
45+
// - syscall.ERROR_ACCESS_DENIED
46+
// - syscall.ERROR_FILE_NOT_FOUND
47+
// - internal/syscall/windows.ERROR_SHARING_VIOLATION
48+
//
49+
// This set may be expanded in the future; programs must not rely on the
50+
// non-ephemerality of any given error.
51+
func IsEphemeralError(err error) bool {
52+
return isEphemeralError(err)
53+
}

src/cmd/go/internal/renameio/rename.go renamed to src/cmd/go/internal/robustio/robustio_other.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
//+build !windows
66

7-
package renameio
7+
package robustio
88

99
import (
1010
"io/ioutil"
@@ -19,6 +19,10 @@ func readFile(filename string) ([]byte, error) {
1919
return ioutil.ReadFile(filename)
2020
}
2121

22+
func removeAll(path string) error {
23+
return os.RemoveAll(path)
24+
}
25+
2226
func isEphemeralError(err error) bool {
2327
return false
2428
}

src/cmd/go/internal/renameio/rename_windows.go renamed to src/cmd/go/internal/robustio/robustio_windows.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
package renameio
5+
package robustio
66

77
import (
88
"errors"
@@ -14,9 +14,10 @@ import (
1414
"time"
1515
)
1616

17+
const arbitraryTimeout = 500 * time.Millisecond
18+
1719
// retry retries ephemeral errors from f up to an arbitrary timeout
1820
// to work around spurious filesystem errors on Windows
19-
// (see golang.org/issue/31247 and golang.org/issue/32188).
2021
func retry(f func() (err error, mayRetry bool)) error {
2122
var (
2223
bestErr error
@@ -40,7 +41,7 @@ func retry(f func() (err error, mayRetry bool)) error {
4041

4142
if start.IsZero() {
4243
start = time.Now()
43-
} else if d := time.Since(start) + nextSleep; d >= 500*time.Millisecond {
44+
} else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout {
4445
break
4546
}
4647
time.Sleep(nextSleep)
@@ -61,8 +62,6 @@ func retry(f func() (err error, mayRetry bool)) error {
6162
//
6263
// Empirical error rates with MoveFileEx are lower under modest concurrency, so
6364
// for now we're sticking with what the os package already provides.
64-
//
65-
// TODO(bcmills): For Go 1.14, should we try changing os.Rename itself to do this?
6665
func rename(oldpath, newpath string) (err error) {
6766
return retry(func() (err error, mayRetry bool) {
6867
err = os.Rename(oldpath, newpath)
@@ -71,8 +70,6 @@ func rename(oldpath, newpath string) (err error) {
7170
}
7271

7372
// readFile is like ioutil.ReadFile, but retries ephemeral errors.
74-
//
75-
// TODO(bcmills): For Go 1.14, should we try changing ioutil.ReadFile itself to do this?
7673
func readFile(filename string) ([]byte, error) {
7774
var b []byte
7875
err := retry(func() (err error, mayRetry bool) {
@@ -86,6 +83,13 @@ func readFile(filename string) ([]byte, error) {
8683
return b, err
8784
}
8885

86+
func removeAll(path string) error {
87+
return retry(func() (err error, mayRetry bool) {
88+
err = os.RemoveAll(path)
89+
return err, isEphemeralError(err)
90+
})
91+
}
92+
8993
// isEphemeralError returns true if err may be resolved by waiting.
9094
func isEphemeralError(err error) bool {
9195
var errno syscall.Errno

src/cmd/go/script_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"cmd/go/internal/cfg"
2828
"cmd/go/internal/imports"
2929
"cmd/go/internal/par"
30+
"cmd/go/internal/robustio"
3031
"cmd/go/internal/txtar"
3132
"cmd/go/internal/work"
3233
)
@@ -388,7 +389,7 @@ func (ts *testScript) cmdCc(neg bool, args []string) {
388389
var b work.Builder
389390
b.Init()
390391
ts.cmdExec(neg, append(b.GccCmd(".", ""), args...))
391-
os.RemoveAll(b.WorkDir)
392+
robustio.RemoveAll(b.WorkDir)
392393
}
393394

394395
// cd changes to a different directory.
@@ -669,8 +670,8 @@ func (ts *testScript) cmdRm(neg bool, args []string) {
669670
}
670671
for _, arg := range args {
671672
file := ts.mkabs(arg)
672-
removeAll(file) // does chmod and then attempts rm
673-
ts.check(os.RemoveAll(file)) // report error
673+
removeAll(file) // does chmod and then attempts rm
674+
ts.check(robustio.RemoveAll(file)) // report error
674675
}
675676
}
676677

0 commit comments

Comments
 (0)