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
7package robustio
8
9import (
10	"errors"
11	"math/rand"
12	"os"
13	"syscall"
14	"time"
15)
16
17const 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.
21func 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.
65func 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.
73func 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
86func 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