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.
36package durable
47
58import (
@@ -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.
5667func 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