package common import ( fmt "fmt" "io" "os" "path/filepath" "strconv" "strings" "sync" "time" ) const ( atomicWriteFilePrefix = "write-file-atomic-" // Maximum number of atomic write file conflicts before we start reseeding // (reduced from golang's default 10 due to using an increased randomness space) atomicWriteFileMaxNumConflicts = 5 // Maximum number of attempts to make at writing the write file before giving up // (reduced from golang's default 10000 due to using an increased randomness space) atomicWriteFileMaxNumWriteAttempts = 1000 // LCG constants from Donald Knuth MMIX // This LCG's has a period equal to 2**64 lcgA = 6364136223846793005 lcgC = 1442695040888963407 // Create in case it doesn't exist and force kernel // flush, which still leaves the potential of lingering disk cache. // Never overwrites files atomicWriteFileFlag = os.O_WRONLY | os.O_CREATE | os.O_SYNC | os.O_TRUNC | os.O_EXCL ) var ( atomicWriteFileRand uint64 atomicWriteFileRandMu sync.Mutex ) func writeFileRandReseed() uint64 { // Scale the PID, to minimize the chance that two processes seeded at similar times // don't get the same seed. Note that PID typically ranges in [0, 2**15), but can be // up to 2**22 under certain configurations. We left bit-shift the PID by 20, so that // a PID difference of one corresponds to a time difference of 2048 seconds. // The important thing here is that now for a seed conflict, they would both have to be on // the correct nanosecond offset, and second-based offset, which is much less likely than // just a conflict with the correct nanosecond offset. return uint64(time.Now().UnixNano() + int64(os.Getpid()<<20)) } // Use a fast thread safe LCG for atomic write file names. // Returns a string corresponding to a 64 bit int. // If it was a negative int, the leading number is a 0. func randWriteFileSuffix() string { atomicWriteFileRandMu.Lock() r := atomicWriteFileRand if r == 0 { r = writeFileRandReseed() } // Update randomness according to lcg r = r*lcgA + lcgC atomicWriteFileRand = r atomicWriteFileRandMu.Unlock() // Can have a negative name, replace this in the following suffix := strconv.Itoa(int(r)) if string(suffix[0]) == "-" { // Replace first "-" with "0". This is purely for UI clarity, // as otherwhise there would be two `-` in a row. suffix = strings.Replace(suffix, "-", "0", 1) } return suffix } // WriteFileAtomic creates a temporary file with data and provided perm and // swaps it atomically with filename if successful. func WriteFileAtomic(filename string, data []byte, perm os.FileMode) (err error) { // This implementation is inspired by the golang stdlibs method of creating // tempfiles. Notable differences are that we use different flags, a 64 bit LCG // and handle negatives differently. // The core reason we can't use golang's TempFile is that we must write // to the file synchronously, as we need this to persist to disk. // We also open it in write-only mode, to avoid concerns that arise with read. var ( dir = filepath.Dir(filename) f *os.File ) nconflict := 0 // Limit the number of attempts to create a file. Something is seriously // wrong if it didn't get created after 1000 attempts, and we don't want // an infinite loop i := 0 for ; i < atomicWriteFileMaxNumWriteAttempts; i++ { name := filepath.Join(dir, atomicWriteFilePrefix+randWriteFileSuffix()) f, err = os.OpenFile(name, atomicWriteFileFlag, perm) // If the file already exists, try a new file if os.IsExist(err) { // If the files exists too many times, start reseeding as we've // likely hit another instances seed. if nconflict++; nconflict > atomicWriteFileMaxNumConflicts { atomicWriteFileRandMu.Lock() atomicWriteFileRand = writeFileRandReseed() atomicWriteFileRandMu.Unlock() } continue } else if err != nil { return err } break } if i == atomicWriteFileMaxNumWriteAttempts { return fmt.Errorf("Could not create atomic write file after %d attempts", i) } // Clean up in any case. Defer stacking order is last-in-first-out. defer os.Remove(f.Name()) defer f.Close() if n, err := f.Write(data); err != nil { return err } else if n < len(data) { return io.ErrShortWrite } // Close the file before renaming it, otherwise it will cause "The process // cannot access the file because it is being used by another process." on windows. f.Close() return os.Rename(f.Name(), filename) }