When we put something in the queue and respond "250 ok" to the client, that is taken as accepting the email. As part of putting something in the queue, we write it to disk, but today we don't do an fsync on that file. That leaves a gap where a badly timed crash on some systems could lead to the file being empty, causing us to lose an email that we accepted. To elliminate (or drastically reduce on some filesystems) the chances of that situation, we call fsync on the file that gets written when we put something in the queue. Thanks to nolanl@github for reporting this in https://github.com/albertito/chasquid/issues/78.
116 lines
2.9 KiB
Go
116 lines
2.9 KiB
Go
// Package safeio implements convenient I/O routines that provide additional
|
|
// levels of safety in the presence of unexpected failures.
|
|
package safeio
|
|
|
|
import (
|
|
"os"
|
|
"path"
|
|
"syscall"
|
|
)
|
|
|
|
// osFile is an interface to the methods of os.File that we need, so we can
|
|
// simulate failures in tests.
|
|
type osFile interface {
|
|
Name() string
|
|
Chmod(os.FileMode) error
|
|
Chown(int, int) error
|
|
Write([]byte) (int, error)
|
|
Close() error
|
|
}
|
|
|
|
var createTemp func(dir, pattern string) (osFile, error) = func(
|
|
dir, pattern string) (osFile, error) {
|
|
return os.CreateTemp(dir, pattern)
|
|
}
|
|
|
|
// FileOp represents an operation on a file (passed by its name).
|
|
type FileOp func(fname string) error
|
|
|
|
// WriteFile writes data to a file named by filename, atomically.
|
|
//
|
|
// It's a wrapper to os.WriteFile, but provides atomicity (and increased
|
|
// safety) by writing to a temporary file and renaming it at the end.
|
|
//
|
|
// Before the final rename, the given ops (if any) are called. They can be
|
|
// used to manipulate the file before it is atomically renamed.
|
|
// If any operation fails, the file is removed and the error is returned.
|
|
//
|
|
// Note this relies on same-directory Rename being atomic, which holds in most
|
|
// reasonably modern filesystems.
|
|
func WriteFile(filename string, data []byte, perm os.FileMode, ops ...FileOp) error {
|
|
// Note we create the temporary file in the same directory, otherwise we
|
|
// would have no expectation of Rename being atomic.
|
|
// We make the file names start with "." so there's no confusion with the
|
|
// originals.
|
|
tmpf, err := createTemp(path.Dir(filename), "."+path.Base(filename))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = tmpf.Chmod(perm); err != nil {
|
|
tmpf.Close()
|
|
os.Remove(tmpf.Name())
|
|
return err
|
|
}
|
|
|
|
if uid, gid := getOwner(filename); uid >= 0 {
|
|
if err = tmpf.Chown(uid, gid); err != nil {
|
|
tmpf.Close()
|
|
os.Remove(tmpf.Name())
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err = tmpf.Write(data); err != nil {
|
|
tmpf.Close()
|
|
os.Remove(tmpf.Name())
|
|
return err
|
|
}
|
|
|
|
if err = tmpf.Close(); err != nil {
|
|
os.Remove(tmpf.Name())
|
|
return err
|
|
}
|
|
|
|
for _, op := range ops {
|
|
if err = op(tmpf.Name()); err != nil {
|
|
os.Remove(tmpf.Name())
|
|
return err
|
|
}
|
|
}
|
|
|
|
return os.Rename(tmpf.Name(), filename)
|
|
}
|
|
|
|
func getOwner(fname string) (uid, gid int) {
|
|
uid = -1
|
|
gid = -1
|
|
stat, err := os.Stat(fname)
|
|
if err == nil {
|
|
if sysstat, ok := stat.Sys().(*syscall.Stat_t); ok {
|
|
uid = int(sysstat.Uid)
|
|
gid = int(sysstat.Gid)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// FsyncFileOp returns a FileOp that fsyncs the given file.
|
|
// It would be even better if we would do this on the open file, but
|
|
// unfortunately we don't have the API for it just yet. This should be good
|
|
// enough for most cases on modern filesystems anyway.
|
|
func FsyncFileOp(fname string) error {
|
|
f, err := os.OpenFile(fname, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = f.Sync(); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
|
|
return f.Close()
|
|
}
|