Skip to content

Commit 7ac5d8e

Browse files
committed
internal/durable: improve resiliency to write-through errors
1 parent 0cc1576 commit 7ac5d8e

File tree

1 file changed

+48
-37
lines changed

1 file changed

+48
-37
lines changed

internal/durable/path.go

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// Package durable provides equivalent functionality to the os package, but with
22
// additional guarantees for durability based on [os.File.Sync].
3+
//
4+
// See https://wiki.postgresql.org/wiki/Fsync_Errors for a discussion of the
5+
// reliability of fsync.
36
package durable
47

58
import (
@@ -10,30 +13,35 @@ import (
1013

1114
// WriteFile behaves like [os.WriteFile], but it also syncs the file contents
1215
// and the directory containing the file to disk.
13-
func WriteFile(name string, data []byte, perm os.FileMode) error {
16+
func WriteFile(name string, data []byte, perm os.FileMode) (err error) {
17+
// After the file, sync the directory in which the entry resides. Otherwise,
18+
// after a power failure the file may not exist.
19+
//
20+
// Keep the parent open during the operation, to prevent it from being
21+
// evicted from the inode cache, which can discard a write-through error.
22+
//
23+
// Note that this is not necessary if the file already exists. Our use of
24+
// this function is mostly for creating new files, so we don't optimize for
25+
// the case where the file already exists.
26+
parent, err := os.Open(filepath.Dir(name))
27+
if err != nil {
28+
return &os.PathError{Op: "write", Path: name, Err: err}
29+
}
30+
defer fsyncAndClose(parent, &err)
31+
1432
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
1533
if err != nil {
1634
return err
1735
}
36+
defer fsyncAndClose(f, &err)
37+
1838
_, err = f.Write(data)
19-
if err == nil {
20-
err = f.Sync()
21-
}
22-
if err1 := f.Close(); err1 != nil && err == nil {
23-
err = err1
24-
}
25-
if err == nil {
26-
// Note that this is not necessary if the file was not just created.
27-
// Our use of this function is mostly for creating new files, so we
28-
// don't optimize for the case where the file already exists.
29-
err = fsyncDirectory(name)
30-
}
3139
return err
3240
}
3341

34-
// MkdirAll behaves like [os.MkdirAll], but it also syncs the directory
35-
// containing each created directory to disk.
36-
func MkdirAll(path string, perm os.FileMode) error {
42+
// MkdirAll behaves like [os.MkdirAll], but it also syncs each affected
43+
// directory to disk.
44+
func MkdirAll(path string, perm os.FileMode) (err error) {
3745
if dir, err := os.Stat(path); err == nil {
3846
if dir.IsDir() {
3947
return nil
@@ -48,36 +56,39 @@ func MkdirAll(path string, perm os.FileMode) error {
4856
}
4957
}
5058

59+
// This is a little inefficient because if we are creating two levels of
60+
// directories, we will sync the intermediate directory twice. That's going
61+
// to be rare in our use case, so we don't optimize for it.
5162
return Mkdir(path, perm)
5263
}
5364

5465
// Mkdir behaves like [os.Mkdir], but it also syncs the directory containing
5566
// the created directory to disk.
5667
func Mkdir(path string, perm os.FileMode) (err error) {
57-
defer func() {
58-
if err == nil {
59-
err = fsyncDirectory(path)
60-
}
61-
}()
62-
63-
// Do we need to sync path itself? Presumably not, since otherwise the
64-
// synced parent directory would have a dangling entry.
65-
return os.Mkdir(path, perm)
66-
}
67-
68-
// fsyncDirectory syncs the directory in which the directory entry for path
69-
// resides. Otherwise, after a power failure the file at path may not exist.
70-
// See https://github.com/sqlite/sqlite/blob/024818be2/src/os_unix.c#L3739-L3799
71-
// for confirmation that operating on a file, then opening the directory and
72-
// calling fsync on it is the correct sequence of operations.
73-
func fsyncDirectory(path string) error {
7468
parent, err := os.Open(filepath.Dir(path))
7569
if err != nil {
70+
return &os.PathError{Op: "mkdir", Path: path, Err: err}
71+
}
72+
defer fsyncAndClose(parent, &err)
73+
74+
if err := os.Mkdir(path, perm); err != nil {
7675
return err
7776
}
78-
err = parent.Sync()
79-
if err1 := parent.Close(); err1 != nil && err == nil {
80-
err = err1
77+
78+
f, err := os.Open(path)
79+
if err != nil {
80+
return &os.PathError{Op: "mkdir", Path: path, Err: err}
81+
}
82+
defer fsyncAndClose(f, &err)
83+
84+
return nil
85+
}
86+
87+
func fsyncAndClose(f *os.File, err *error) {
88+
if *err == nil {
89+
*err = f.Sync()
90+
}
91+
if err1 := f.Close(); err1 != nil && *err == nil {
92+
*err = err1
8193
}
82-
return err
8394
}

0 commit comments

Comments
 (0)