robustio_flaky.go raw

   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