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 //go:build windows || darwin
6 7 package robustio
8 9 import (
10 "errors"
11 "math/rand"
12 "os"
13 "syscall"
14 "time"
15 )
16 17 const arbitraryTimeout = 2000 * time.Millisecond
18 19 // retry retries ephemeral errors from f up to an arbitrary timeout
20 // to work around filesystem flakiness on Windows and Darwin.
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 // On Windows 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 os.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 = os.ReadFile(filename)
77 78 // Unlike in rename, we do not retry errFileNotFound 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, errFileNotFound)
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 }
92