github.com/pure-x-eth/consensus_tm@v0.0.0-20230502163723-e3c2ff987250/libs/tempfile/tempfile.go (about)

     1  package tempfile
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	tmsync "github.com/pure-x-eth/consensus_tm/libs/sync"
    13  )
    14  
    15  const (
    16  	atomicWriteFilePrefix = "write-file-atomic-"
    17  	// Maximum number of atomic write file conflicts before we start reseeding
    18  	// (reduced from golang's default 10 due to using an increased randomness space)
    19  	atomicWriteFileMaxNumConflicts = 5
    20  	// Maximum number of attempts to make at writing the write file before giving up
    21  	// (reduced from golang's default 10000 due to using an increased randomness space)
    22  	atomicWriteFileMaxNumWriteAttempts = 1000
    23  	// LCG constants from Donald Knuth MMIX
    24  	// This LCG's has a period equal to 2**64
    25  	lcgA = 6364136223846793005
    26  	lcgC = 1442695040888963407
    27  	// Create in case it doesn't exist and force kernel
    28  	// flush, which still leaves the potential of lingering disk cache.
    29  	// Never overwrites files
    30  	atomicWriteFileFlag = os.O_WRONLY | os.O_CREATE | os.O_SYNC | os.O_TRUNC | os.O_EXCL
    31  )
    32  
    33  var (
    34  	atomicWriteFileRand   uint64
    35  	atomicWriteFileRandMu tmsync.Mutex
    36  )
    37  
    38  func writeFileRandReseed() uint64 {
    39  	// Scale the PID, to minimize the chance that two processes seeded at similar times
    40  	// don't get the same seed. Note that PID typically ranges in [0, 2**15), but can be
    41  	// up to 2**22 under certain configurations. We left bit-shift the PID by 20, so that
    42  	// a PID difference of one corresponds to a time difference of 2048 seconds.
    43  	// The important thing here is that now for a seed conflict, they would both have to be on
    44  	// the correct nanosecond offset, and second-based offset, which is much less likely than
    45  	// just a conflict with the correct nanosecond offset.
    46  	return uint64(time.Now().UnixNano() + int64(os.Getpid()<<20))
    47  }
    48  
    49  // Use a fast thread safe LCG for atomic write file names.
    50  // Returns a string corresponding to a 64 bit int.
    51  // If it was a negative int, the leading number is a 0.
    52  func randWriteFileSuffix() string {
    53  	atomicWriteFileRandMu.Lock()
    54  	r := atomicWriteFileRand
    55  	if r == 0 {
    56  		r = writeFileRandReseed()
    57  	}
    58  
    59  	// Update randomness according to lcg
    60  	r = r*lcgA + lcgC
    61  
    62  	atomicWriteFileRand = r
    63  	atomicWriteFileRandMu.Unlock()
    64  	// Can have a negative name, replace this in the following
    65  	suffix := strconv.Itoa(int(r))
    66  	if string(suffix[0]) == "-" {
    67  		// Replace first "-" with "0". This is purely for UI clarity,
    68  		// as otherwhise there would be two `-` in a row.
    69  		suffix = strings.Replace(suffix, "-", "0", 1)
    70  	}
    71  	return suffix
    72  }
    73  
    74  // WriteFileAtomic creates a temporary file with data and provided perm and
    75  // swaps it atomically with filename if successful.
    76  func WriteFileAtomic(filename string, data []byte, perm os.FileMode) (err error) {
    77  	// This implementation is inspired by the golang stdlibs method of creating
    78  	// tempfiles. Notable differences are that we use different flags, a 64 bit LCG
    79  	// and handle negatives differently.
    80  	// The core reason we can't use golang's TempFile is that we must write
    81  	// to the file synchronously, as we need this to persist to disk.
    82  	// We also open it in write-only mode, to avoid concerns that arise with read.
    83  	var (
    84  		dir = filepath.Dir(filename)
    85  		f   *os.File
    86  	)
    87  
    88  	nconflict := 0
    89  	// Limit the number of attempts to create a file. Something is seriously
    90  	// wrong if it didn't get created after 1000 attempts, and we don't want
    91  	// an infinite loop
    92  	i := 0
    93  	for ; i < atomicWriteFileMaxNumWriteAttempts; i++ {
    94  		name := filepath.Join(dir, atomicWriteFilePrefix+randWriteFileSuffix())
    95  		f, err = os.OpenFile(name, atomicWriteFileFlag, perm)
    96  		// If the file already exists, try a new file
    97  		if os.IsExist(err) {
    98  			// If the files exists too many times, start reseeding as we've
    99  			// likely hit another instances seed.
   100  			if nconflict++; nconflict > atomicWriteFileMaxNumConflicts {
   101  				atomicWriteFileRandMu.Lock()
   102  				atomicWriteFileRand = writeFileRandReseed()
   103  				atomicWriteFileRandMu.Unlock()
   104  			}
   105  			continue
   106  		} else if err != nil {
   107  			return err
   108  		}
   109  		break
   110  	}
   111  	if i == atomicWriteFileMaxNumWriteAttempts {
   112  		return fmt.Errorf("could not create atomic write file after %d attempts", i)
   113  	}
   114  
   115  	// Clean up in any case. Defer stacking order is last-in-first-out.
   116  	defer os.Remove(f.Name())
   117  	defer f.Close()
   118  
   119  	if n, err := f.Write(data); err != nil {
   120  		return err
   121  	} else if n < len(data) {
   122  		return io.ErrShortWrite
   123  	}
   124  	// Close the file before renaming it, otherwise it will cause "The process
   125  	// cannot access the file because it is being used by another process." on windows.
   126  	f.Close()
   127  
   128  	return os.Rename(f.Name(), filename)
   129  }