-
Notifications
You must be signed in to change notification settings - Fork 14
/
tempfile.go
129 lines (116 loc) · 4.4 KB
/
tempfile.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package tempfile
import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
sync "github.com/sasha-s/go-deadlock"
)
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)
}