Skip to content

Commit 81c6bac

Browse files
author
Bryan C. Mills
committed
cmd/go/internal/robustio: extend filesystem workarounds to darwin platforms
The macOS filesystem seems to have gotten significantly flakier as of macOS 10.14, so this causes frequently flakes in the 10.14 builders. We have no reason to believe that it will be fixed any time soon, so rather than trying to detect the specific macOS version, we'll apply the same workarounds that we use on Windows: classifying (and retrying) the errors known to indicate flakiness and relaxing the success criteria for renameio.TestConcurrentReadsAndWrites. Fixes #33041 Change-Id: I74d8c15677951d7a0df0d4ebf6ea03e43eebddf9 Reviewed-on: https://go-review.googlesource.com/c/go/+/197517 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 7defbff commit 81c6bac

File tree

5 files changed

+125
-82
lines changed

5 files changed

+125
-82
lines changed

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,18 @@ func TestConcurrentReadsAndWrites(t *testing.T) {
131131
}
132132

133133
var minReadSuccesses int64 = attempts
134-
if runtime.GOOS == "windows" {
134+
135+
switch runtime.GOOS {
136+
case "windows":
135137
// Windows produces frequent "Access is denied" errors under heavy rename load.
136-
// As long as those are the only errors and *some* of the writes succeed, we're happy.
138+
// As long as those are the only errors and *some* of the reads succeed, we're happy.
137139
minReadSuccesses = attempts / 4
140+
141+
case "darwin":
142+
// The filesystem on macOS 10.14 occasionally fails with "no such file or
143+
// directory" errors. See https://golang.org/issue/33041. The flake rate is
144+
// fairly low, so ensure that at least 75% of attempts succeed.
145+
minReadSuccesses = attempts - (attempts / 4)
138146
}
139147

140148
if readSuccesses < minReadSuccesses {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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
6+
7+
import (
8+
"errors"
9+
"syscall"
10+
)
11+
12+
const errFileNotFound = syscall.ENOENT
13+
14+
// isEphemeralError returns true if err may be resolved by waiting.
15+
func isEphemeralError(err error) bool {
16+
var errno syscall.Errno
17+
if errors.As(err, &errno) {
18+
return errno == errFileNotFound
19+
}
20+
return false
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
// +build windows darwin
6+
7+
package robustio
8+
9+
import (
10+
"errors"
11+
"io/ioutil"
12+
"math/rand"
13+
"os"
14+
"syscall"
15+
"time"
16+
)
17+
18+
const arbitraryTimeout = 500 * time.Millisecond
19+
20+
// retry retries ephemeral errors from f up to an arbitrary timeout
21+
// to work around filesystem flakiness on Windows and Darwin.
22+
func retry(f func() (err error, mayRetry bool)) error {
23+
var (
24+
bestErr error
25+
lowestErrno syscall.Errno
26+
start time.Time
27+
nextSleep time.Duration = 1 * time.Millisecond
28+
)
29+
for {
30+
err, mayRetry := f()
31+
if err == nil || !mayRetry {
32+
return err
33+
}
34+
35+
var errno syscall.Errno
36+
if errors.As(err, &errno) && (lowestErrno == 0 || errno < lowestErrno) {
37+
bestErr = err
38+
lowestErrno = errno
39+
} else if bestErr == nil {
40+
bestErr = err
41+
}
42+
43+
if start.IsZero() {
44+
start = time.Now()
45+
} else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout {
46+
break
47+
}
48+
time.Sleep(nextSleep)
49+
nextSleep += time.Duration(rand.Int63n(int64(nextSleep)))
50+
}
51+
52+
return bestErr
53+
}
54+
55+
// rename is like os.Rename, but retries ephemeral errors.
56+
//
57+
// On Windows it wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with
58+
// MOVEFILE_REPLACE_EXISTING.
59+
//
60+
// Windows also provides a different system call, ReplaceFile,
61+
// that provides similar semantics, but perhaps preserves more metadata. (The
62+
// documentation on the differences between the two is very sparse.)
63+
//
64+
// Empirical error rates with MoveFileEx are lower under modest concurrency, so
65+
// for now we're sticking with what the os package already provides.
66+
func rename(oldpath, newpath string) (err error) {
67+
return retry(func() (err error, mayRetry bool) {
68+
err = os.Rename(oldpath, newpath)
69+
return err, isEphemeralError(err)
70+
})
71+
}
72+
73+
// readFile is like ioutil.ReadFile, but retries ephemeral errors.
74+
func readFile(filename string) ([]byte, error) {
75+
var b []byte
76+
err := retry(func() (err error, mayRetry bool) {
77+
b, err = ioutil.ReadFile(filename)
78+
79+
// Unlike in rename, we do not retry errFileNotFound here: it can occur
80+
// as a spurious error, but the file may also genuinely not exist, so the
81+
// increase in robustness is probably not worth the extra latency.
82+
return err, isEphemeralError(err) && !errors.Is(err, errFileNotFound)
83+
})
84+
return b, err
85+
}
86+
87+
func removeAll(path string) error {
88+
return retry(func() (err error, mayRetry bool) {
89+
err = os.RemoveAll(path)
90+
return err, isEphemeralError(err)
91+
})
92+
}

src/cmd/go/internal/robustio/robustio_other.go

+1-1
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-
// +build !windows
5+
// +build !windows,!darwin
66

77
package robustio
88

src/cmd/go/internal/robustio/robustio_windows.go

+1-79
Original file line numberDiff line numberDiff line change
@@ -7,88 +7,10 @@ package robustio
77
import (
88
"errors"
99
"internal/syscall/windows"
10-
"io/ioutil"
11-
"math/rand"
12-
"os"
1310
"syscall"
14-
"time"
1511
)
1612

17-
const arbitraryTimeout = 500 * time.Millisecond
18-
19-
// retry retries ephemeral errors from f up to an arbitrary timeout
20-
// to work around spurious filesystem errors on Windows
21-
func retry(f func() (err error, mayRetry bool)) error {
22-
var (
23-
bestErr error
24-
lowestErrno syscall.Errno
25-
start time.Time
26-
nextSleep time.Duration = 1 * time.Millisecond
27-
)
28-
for {
29-
err, mayRetry := f()
30-
if err == nil || !mayRetry {
31-
return err
32-
}
33-
34-
var errno syscall.Errno
35-
if errors.As(err, &errno) && (lowestErrno == 0 || errno < lowestErrno) {
36-
bestErr = err
37-
lowestErrno = errno
38-
} else if bestErr == nil {
39-
bestErr = err
40-
}
41-
42-
if start.IsZero() {
43-
start = time.Now()
44-
} else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout {
45-
break
46-
}
47-
time.Sleep(nextSleep)
48-
nextSleep += time.Duration(rand.Int63n(int64(nextSleep)))
49-
}
50-
51-
return bestErr
52-
}
53-
54-
// rename is like os.Rename, but retries ephemeral errors.
55-
//
56-
// It wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with
57-
// MOVEFILE_REPLACE_EXISTING.
58-
//
59-
// Windows also provides a different system call, ReplaceFile,
60-
// that provides similar semantics, but perhaps preserves more metadata. (The
61-
// documentation on the differences between the two is very sparse.)
62-
//
63-
// Empirical error rates with MoveFileEx are lower under modest concurrency, so
64-
// for now we're sticking with what the os package already provides.
65-
func rename(oldpath, newpath string) (err error) {
66-
return retry(func() (err error, mayRetry bool) {
67-
err = os.Rename(oldpath, newpath)
68-
return err, isEphemeralError(err)
69-
})
70-
}
71-
72-
// readFile is like ioutil.ReadFile, but retries ephemeral errors.
73-
func readFile(filename string) ([]byte, error) {
74-
var b []byte
75-
err := retry(func() (err error, mayRetry bool) {
76-
b, err = ioutil.ReadFile(filename)
77-
78-
// Unlike in rename, we do not retry ERROR_FILE_NOT_FOUND here: it can occur
79-
// as a spurious error, but the file may also genuinely not exist, so the
80-
// increase in robustness is probably not worth the extra latency.
81-
return err, isEphemeralError(err) && !errors.Is(err, syscall.ERROR_FILE_NOT_FOUND)
82-
})
83-
return b, err
84-
}
85-
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-
}
13+
const errFileNotFound = syscall.ERROR_FILE_NOT_FOUND
9214

9315
// isEphemeralError returns true if err may be resolved by waiting.
9416
func isEphemeralError(err error) bool {

0 commit comments

Comments
 (0)