Skip to content

Commit 7c9f12e

Browse files
authored
Hold FD open for directory while making changes within (#750)
1 parent 9634dd4 commit 7c9f12e

1 file changed

Lines changed: 64 additions & 44 deletions

File tree

storage/posix/file_ops.go

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,45 @@ const (
3232
filePerm = 0o644
3333
)
3434

35-
// syncDir calls fsync on the provided path.
35+
// syncDir opens the specified directory and calls op before syncing and closing the handle on the directory.
3636
//
37-
// This is intended to be used to sync directories in which we've just created new entries.
38-
func syncDir(d string) error {
39-
fd, err := os.Open(d)
37+
// This dance ensures that the inode of the specified directory cannot be evicted from the kernel inode cache while
38+
// the operation is underway, and so any error which occurs while updating metadata about a file operation which happens
39+
// _within_ that directory is detected.
40+
//
41+
// This function is intended to be used by the other functions in this file.
42+
func syncDir(dir string, op func() error) (err error) {
43+
fd, err := os.OpenFile(dir, os.O_RDONLY|syscall.O_DIRECTORY, 0)
4044
if err != nil {
41-
return fmt.Errorf("failed to open %q: %v", d, err)
45+
return fmt.Errorf("failed to open %q: %w", dir, err)
46+
}
47+
defer func() {
48+
e := fd.Close()
49+
if err == nil {
50+
err = e
51+
}
52+
}()
53+
54+
if err := op(); err != nil {
55+
return err
4256
}
4357

4458
if err := fd.Sync(); err != nil {
45-
return fmt.Errorf("failed to sync %q: %v", d, err)
59+
return fmt.Errorf("failed to sync %q: %w", dir, err)
4660
}
47-
return fd.Close()
61+
return nil
4862
}
4963

5064
// mkdirAll is a reimplementation of os.mkdirAll but where we fsync the parent directory/ies
5165
// we modify.
52-
func mkdirAll(name string, perm os.FileMode) (err error) {
66+
func mkdirAll(name string, perm os.FileMode) error {
5367
name = strings.TrimSuffix(name, string(filepath.Separator))
5468
if name == "" {
5569
return nil
5670
}
5771

5872
// Finally, check and create the dir if necessary.
59-
dir, _ := filepath.Split(name)
73+
dir := filepath.Dir(name)
6074
di, err := os.Lstat(name)
6175
switch {
6276
case errors.Is(err, syscall.ENOENT):
@@ -72,13 +86,14 @@ func mkdirAll(name string, perm os.FileMode) (err error) {
7286
// create the final entry in the requested path.
7387
fallthrough
7488
case errors.Is(err, os.ErrNotExist):
75-
// We'll see ErrNotExist if the final entry in the requested path doesn't exist,
76-
// so we simply attempt to create it in here.
77-
if err := os.Mkdir(name, perm); err != nil {
78-
return fmt.Errorf("%q: %v", name, err)
79-
}
80-
// And be sure to sync the parent directory.
81-
return syncDir(dir)
89+
return syncDir(dir, func() error {
90+
// We'll see ErrNotExist if the final entry in the requested path doesn't exist,
91+
// so we simply attempt to create it in here.
92+
if err := os.Mkdir(name, perm); err != nil {
93+
return fmt.Errorf("%q: %v", name, err)
94+
}
95+
return nil
96+
})
8297
case err != nil:
8398
return fmt.Errorf("lstat %q: %v", name, err)
8499
case !di.IsDir():
@@ -94,47 +109,52 @@ func mkdirAll(name string, perm os.FileMode) (err error) {
94109
// Returns an error if a file already exists at the specified location, or it's unable to fully write the
95110
// data & close the file.
96111
func createEx(name string, d []byte) error {
97-
dir, _ := filepath.Split(name)
112+
dir := filepath.Dir(name)
98113
if err := mkdirAll(dir, dirPerm); err != nil {
99-
return fmt.Errorf("failed to make entries directory structure: %w", err)
114+
return fmt.Errorf("failed to make directory structure: %w", err)
100115
}
101-
102-
tmpName, err := createTemp(name, d)
103-
if err != nil {
104-
return fmt.Errorf("failed to create temp file: %v", err)
105-
}
106-
defer func() {
107-
if err := os.Remove(tmpName); err != nil {
108-
klog.Warningf("Failed to remove temporary file %q: %v", tmpName, err)
116+
return syncDir(dir, func() error {
117+
tmpName, err := createTemp(name, d)
118+
if err != nil {
119+
return fmt.Errorf("failed to create temp file: %v", err)
109120
}
110-
}()
111-
112-
if err := os.Link(tmpName, name); err != nil {
113-
// Wrap the error here because we need to know if it's os.ErrExists at higher levels.
114-
return fmt.Errorf("failed to link temporary file to target %q: %w", name, err)
115-
}
121+
defer func() {
122+
if err := os.Remove(tmpName); err != nil {
123+
klog.Warningf("Failed to remove temporary file %q: %v", tmpName, err)
124+
}
125+
}()
116126

117-
return syncDir(dir)
127+
if err := os.Link(tmpName, name); err != nil {
128+
// Wrap the error here because we need to know if it's os.ErrExists at higher levels.
129+
return fmt.Errorf("failed to link temporary file to target %q: %w", name, err)
130+
}
131+
return nil
132+
})
118133
}
119134

120135
// overwrite atomically creates/overwrites a file at the given path containing the provided data, and syncs
121136
// the directory containing the overwritten/created file.
122137
func overwrite(name string, d []byte) error {
123-
dir, _ := filepath.Split(name)
138+
dir := filepath.Dir(name)
124139
if err := mkdirAll(dir, dirPerm); err != nil {
125-
return fmt.Errorf("failed to make entries directory structure: %w", err)
126-
}
127-
128-
tmpName, err := createTemp(name, d)
129-
if err != nil {
130-
return fmt.Errorf("failed to create temp file: %v", err)
140+
return fmt.Errorf("failed to make directory structure: %w", err)
131141
}
142+
return syncDir(dir, func() error {
143+
dir, _ := filepath.Split(name)
144+
if err := mkdirAll(dir, dirPerm); err != nil {
145+
return fmt.Errorf("failed to make entries directory structure: %w", err)
146+
}
132147

133-
if err := os.Rename(tmpName, name); err != nil {
134-
return fmt.Errorf("failed to rename temporary file to target %q: %w", name, err)
135-
}
148+
tmpName, err := createTemp(name, d)
149+
if err != nil {
150+
return fmt.Errorf("failed to create temp file: %v", err)
151+
}
136152

137-
return syncDir(dir)
153+
if err := os.Rename(tmpName, name); err != nil {
154+
return fmt.Errorf("failed to rename temporary file to target %q: %w", name, err)
155+
}
156+
return nil
157+
})
138158
}
139159

140160
// createTemp creates a new temporary file in the directory dir, with a name based on the provided prefix,

0 commit comments

Comments
 (0)