Skip to content

Commit bf1f4ec

Browse files
author
Bryan C. Mills
committed
cmd/go/internal/renameio: add a ReadFile function
ReadFile is a drop-in replacement for ioutil.ReadFile that works around Windows filesystem flakiness under load. A followup CL will replace uses of ioutil.ReadFile in cmd/go with this function. Updates #32188 Change-Id: I232ba893b132bdc84cd7b0edde436165a69e1aa8 Reviewed-on: https://go-review.googlesource.com/c/go/+/180219 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 5f50914 commit bf1f4ec

File tree

7 files changed

+297
-70
lines changed

7 files changed

+297
-70
lines changed

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

-12
This file was deleted.

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

-23
This file was deleted.
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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
6+
7+
package renameio
8+
9+
import (
10+
"io/ioutil"
11+
"os"
12+
)
13+
14+
func rename(oldpath, newpath string) error {
15+
return os.Rename(oldpath, newpath)
16+
}
17+
18+
func readFile(filename string) ([]byte, error) {
19+
return ioutil.ReadFile(filename)
20+
}
21+
22+
func isEphemeralError(err error) bool {
23+
return false
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 renameio
6+
7+
import (
8+
"errors"
9+
"internal/syscall/windows"
10+
"io/ioutil"
11+
"math/rand"
12+
"os"
13+
"syscall"
14+
"time"
15+
)
16+
17+
// retry retries ephemeral errors from f up to an arbitrary timeout
18+
// to work around spurious filesystem errors on Windows
19+
// (see golang.org/issue/31247 and golang.org/issue/32188).
20+
func retry(f func() (err error, mayRetry bool)) error {
21+
var (
22+
bestErr error
23+
lowestErrno syscall.Errno
24+
start time.Time
25+
nextSleep time.Duration = 1 * time.Millisecond
26+
)
27+
for {
28+
err, mayRetry := f()
29+
if err == nil || !mayRetry {
30+
return err
31+
}
32+
33+
var errno syscall.Errno
34+
if errors.As(err, &errno) && (lowestErrno == 0 || errno < lowestErrno) {
35+
bestErr = err
36+
lowestErrno = errno
37+
} else if bestErr == nil {
38+
bestErr = err
39+
}
40+
41+
if start.IsZero() {
42+
start = time.Now()
43+
} else if d := time.Since(start) + nextSleep; d >= 500*time.Millisecond {
44+
break
45+
}
46+
time.Sleep(nextSleep)
47+
nextSleep += time.Duration(rand.Int63n(int64(nextSleep)))
48+
}
49+
50+
return bestErr
51+
}
52+
53+
// rename is like os.Rename, but retries ephemeral errors.
54+
//
55+
// It wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with
56+
// MOVEFILE_REPLACE_EXISTING.
57+
//
58+
// Windows also provides a different system call, ReplaceFile,
59+
// that provides similar semantics, but perhaps preserves more metadata. (The
60+
// documentation on the differences between the two is very sparse.)
61+
//
62+
// Empirical error rates with MoveFileEx are lower under modest concurrency, so
63+
// 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?
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+
//
75+
// TODO(bcmills): For Go 1.14, should we try changing ioutil.ReadFile itself to do this?
76+
func readFile(filename string) ([]byte, error) {
77+
var b []byte
78+
err := retry(func() (err error, mayRetry bool) {
79+
b, err = ioutil.ReadFile(filename)
80+
81+
// Unlike in rename, we do not retry ERROR_FILE_NOT_FOUND here: it can occur
82+
// as a spurious error, but the file may also genuinely not exist, so the
83+
// increase in robustness is probably not worth the extra latency.
84+
return err, isEphemeralError(err) && !errors.Is(err, syscall.ERROR_FILE_NOT_FOUND)
85+
})
86+
return b, err
87+
}
88+
89+
// isEphemeralError returns true if err may be resolved by waiting.
90+
func isEphemeralError(err error) bool {
91+
var errno syscall.Errno
92+
if errors.As(err, &errno) {
93+
switch errno {
94+
case syscall.ERROR_ACCESS_DENIED,
95+
syscall.ERROR_FILE_NOT_FOUND,
96+
windows.ERROR_SHARING_VIOLATION:
97+
return true
98+
}
99+
}
100+
return false
101+
}

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

+13-17
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"os"
1313
"path/filepath"
1414
"strconv"
15-
"time"
1615
)
1716

1817
const patternSuffix = ".tmp"
@@ -62,23 +61,20 @@ func WriteToFile(filename string, data io.Reader, perm os.FileMode) (err error)
6261
return err
6362
}
6463

65-
var start time.Time
66-
for {
67-
err := os.Rename(f.Name(), filename)
68-
if err == nil || !isAccessDeniedError(err) {
69-
return err
70-
}
64+
return rename(f.Name(), filename)
65+
}
7166

72-
// Windows seems to occasionally trigger spurious "Access is denied" errors
73-
// here (see golang.org/issue/31247). We're not sure why. It's probably
74-
// worth a little extra latency to avoid propagating the spurious errors.
75-
if start.IsZero() {
76-
start = time.Now()
77-
} else if time.Since(start) >= 500*time.Millisecond {
78-
return err
79-
}
80-
time.Sleep(5 * time.Millisecond)
81-
}
67+
// ReadFile is like ioutil.ReadFile, but on Windows retries spurious errors that
68+
// may occur if the file is concurrently replaced.
69+
//
70+
// Errors are classified heuristically and retries are bounded, so even this
71+
// function may occasionally return a spurious error on Windows.
72+
// If so, the error will likely wrap one of:
73+
// - syscall.ERROR_ACCESS_DENIED
74+
// - syscall.ERROR_FILE_NOT_FOUND
75+
// - internal/syscall/windows.ERROR_SHARING_VIOLATION
76+
func ReadFile(filename string) ([]byte, error) {
77+
return readFile(filename)
8278
}
8379

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

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

+117-18
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,142 @@
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 writes files atomically by renaming temporary files.
6-
7-
//+build !nacl,!plan9,!windows,!js
5+
//+build !plan9
86

97
package renameio
108

119
import (
10+
"encoding/binary"
11+
"errors"
1212
"io/ioutil"
13+
"math/rand"
1314
"os"
1415
"path/filepath"
16+
"runtime"
17+
"sync"
18+
"sync/atomic"
1519
"syscall"
1620
"testing"
21+
"time"
1722
)
1823

19-
func TestWriteFileModeAppliesUmask(t *testing.T) {
24+
func TestConcurrentReadsAndWrites(t *testing.T) {
2025
dir, err := ioutil.TempDir("", "renameio")
2126
if err != nil {
22-
t.Fatalf("Failed to create temporary directory: %v", err)
27+
t.Fatal(err)
2328
}
29+
defer os.RemoveAll(dir)
30+
path := filepath.Join(dir, "blob.bin")
2431

25-
const mode = 0644
26-
const umask = 0007
27-
defer syscall.Umask(syscall.Umask(umask))
32+
const chunkWords = 8 << 10
33+
buf := make([]byte, 2*chunkWords*8)
34+
for i := uint64(0); i < 2*chunkWords; i++ {
35+
binary.LittleEndian.PutUint64(buf[i*8:], i)
36+
}
2837

29-
file := filepath.Join(dir, "testWrite")
30-
err = WriteFile(file, []byte("go-build"), mode)
31-
if err != nil {
32-
t.Fatalf("Failed to write file: %v", err)
38+
var attempts int64 = 128
39+
if !testing.Short() {
40+
attempts *= 16
3341
}
34-
defer os.RemoveAll(dir)
42+
const parallel = 32
3543

36-
fi, err := os.Stat(file)
37-
if err != nil {
38-
t.Fatalf("Stat %q (looking for mode %#o): %s", file, mode, err)
44+
var sem = make(chan bool, parallel)
45+
46+
var (
47+
writeSuccesses, readSuccesses int64 // atomic
48+
writeErrnoSeen, readErrnoSeen sync.Map
49+
)
50+
51+
for n := attempts; n > 0; n-- {
52+
sem <- true
53+
go func() {
54+
defer func() { <-sem }()
55+
56+
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
57+
offset := rand.Intn(chunkWords)
58+
chunk := buf[offset*8 : (offset+chunkWords)*8]
59+
if err := WriteFile(path, chunk, 0666); err == nil {
60+
atomic.AddInt64(&writeSuccesses, 1)
61+
} else if isEphemeralError(err) {
62+
var (
63+
errno syscall.Errno
64+
dup bool
65+
)
66+
if errors.As(err, &errno) {
67+
_, dup = writeErrnoSeen.LoadOrStore(errno, true)
68+
}
69+
if !dup {
70+
t.Logf("ephemeral error: %v", err)
71+
}
72+
} else {
73+
t.Errorf("unexpected error: %v", err)
74+
}
75+
76+
time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
77+
data, err := ioutil.ReadFile(path)
78+
if err == nil {
79+
atomic.AddInt64(&readSuccesses, 1)
80+
} else if isEphemeralError(err) {
81+
var (
82+
errno syscall.Errno
83+
dup bool
84+
)
85+
if errors.As(err, &errno) {
86+
_, dup = readErrnoSeen.LoadOrStore(errno, true)
87+
}
88+
if !dup {
89+
t.Logf("ephemeral error: %v", err)
90+
}
91+
return
92+
} else {
93+
t.Errorf("unexpected error: %v", err)
94+
return
95+
}
96+
97+
if len(data) != 8*chunkWords {
98+
t.Errorf("read %d bytes, but each write is a %d-byte file", len(data), 8*chunkWords)
99+
return
100+
}
101+
102+
u := binary.LittleEndian.Uint64(data)
103+
for i := 1; i < chunkWords; i++ {
104+
next := binary.LittleEndian.Uint64(data[i*8:])
105+
if next != u+1 {
106+
t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i)
107+
return
108+
}
109+
u = next
110+
}
111+
}()
112+
}
113+
114+
for n := parallel; n > 0; n-- {
115+
sem <- true
116+
}
117+
118+
var minWriteSuccesses int64 = attempts
119+
if runtime.GOOS == "windows" {
120+
// Windows produces frequent "Access is denied" errors under heavy rename load.
121+
// As long as those are the only errors and *some* of the writes succeed, we're happy.
122+
minWriteSuccesses = attempts / 4
123+
}
124+
125+
if writeSuccesses < minWriteSuccesses {
126+
t.Errorf("%d (of %d) writes succeeded; want ≥ %d", writeSuccesses, attempts, minWriteSuccesses)
127+
} else {
128+
t.Logf("%d (of %d) writes succeeded (ok: ≥ %d)", writeSuccesses, attempts, minWriteSuccesses)
129+
}
130+
131+
var minReadSuccesses int64 = attempts
132+
if runtime.GOOS == "windows" {
133+
// Windows produces frequent "Access is denied" errors under heavy rename load.
134+
// As long as those are the only errors and *some* of the writes succeed, we're happy.
135+
minReadSuccesses = attempts / 4
39136
}
40137

41-
if fi.Mode()&os.ModePerm != 0640 {
42-
t.Errorf("Stat %q: mode %#o want %#o", file, fi.Mode()&os.ModePerm, 0640)
138+
if readSuccesses < minReadSuccesses {
139+
t.Errorf("%d (of %d) reads succeeded; want ≥ %d", readSuccesses, attempts, minReadSuccesses)
140+
} else {
141+
t.Logf("%d (of %d) reads succeeded (ok: ≥ %d)", readSuccesses, attempts, minReadSuccesses)
43142
}
44143
}

0 commit comments

Comments
 (0)