Files
tendermint/internal/libs/autofile/autofile.go
M. J. FrombergerandGitHub a7eb95065d autofile: ensure files are not reopened after closing (#7628)
During file rotation and WAL shutdown, there was a race condition between users
of an autofile and its termination. To fix this, ensure operations on an
autofile are properly synchronized, and report errors when attempting to use an
autofile after it was closed.

Notably:

- Simplify the cancellation protocol between signal and Close.
- Exclude writers to an autofile during rotation.
- Add documentation about what is going on.

There is a lot more that could be improved here, but this addresses the more
obvious races that have been panicking unit tests.
2022-01-18 14:57:20 -08:00

225 lines
5.2 KiB
Go

package autofile
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"time"
tmrand "github.com/tendermint/tendermint/libs/rand"
)
/* AutoFile usage
// Create/Append to ./autofile_test
af, err := OpenAutoFile("autofile_test")
if err != nil {
panic(err)
}
// Stream of writes.
// During this time, the file may be moved e.g. by logRotate.
for i := 0; i < 60; i++ {
af.Write([]byte(Fmt("LOOP(%v)", i)))
time.Sleep(time.Second)
}
// Close the AutoFile
err = af.Close()
if err != nil {
panic(err)
}
*/
const (
autoFileClosePeriod = 1000 * time.Millisecond
autoFilePerms = os.FileMode(0600)
)
// errAutoFileClosed is reported when operations attempt to use an autofile
// after it has been closed.
var errAutoFileClosed = errors.New("autofile is closed")
// AutoFile automatically closes and re-opens file for writing. The file is
// automatically setup to close itself every 1s and upon receiving SIGHUP.
//
// This is useful for using a log file with the logrotate tool.
type AutoFile struct {
ID string
Path string
closeTicker *time.Ticker // signals periodic close
cancel func() // cancels the lifecycle context
mtx sync.Mutex // guards the fields below
closed bool // true when the the autofile is no longer usable
file *os.File // the underlying file (may be nil)
}
// OpenAutoFile creates an AutoFile in the path (with random ID). If there is
// an error, it will be of type *PathError or *ErrPermissionsChanged (if file's
// permissions got changed (should be 0600)).
func OpenAutoFile(ctx context.Context, path string) (*AutoFile, error) {
var err error
path, err = filepath.Abs(path)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(ctx)
af := &AutoFile{
ID: tmrand.Str(12) + ":" + path,
Path: path,
closeTicker: time.NewTicker(autoFileClosePeriod),
cancel: cancel,
}
if err := af.openFile(); err != nil {
af.Close()
return nil, err
}
// Set up a SIGHUP handler to forcibly flush and close the filehandle.
// This forces the next operation to re-open the underlying path.
hupc := make(chan os.Signal, 1)
signal.Notify(hupc, syscall.SIGHUP)
go func() {
defer close(hupc)
for {
select {
case <-hupc:
_ = af.closeFile()
case <-ctx.Done():
return
}
}
}()
go af.closeFileRoutine(ctx)
return af, nil
}
// Close shuts down the service goroutine and marks af as invalid. Operations
// on af after Close will report an error.
func (af *AutoFile) Close() error {
return af.withLock(func() error {
af.cancel() // signal the close service to stop
af.closed = true // mark the file as invalid
return af.unsyncCloseFile()
})
}
func (af *AutoFile) closeFileRoutine(ctx context.Context) {
for {
select {
case <-ctx.Done():
_ = af.Close()
return
case <-af.closeTicker.C:
_ = af.closeFile()
}
}
}
func (af *AutoFile) closeFile() (err error) {
return af.withLock(af.unsyncCloseFile)
}
// unsyncCloseFile closes the underlying filehandle if one is open, and reports
// any error it returns. The caller must hold af.mtx exclusively.
func (af *AutoFile) unsyncCloseFile() error {
if fp := af.file; fp != nil {
af.file = nil
return fp.Close()
}
return nil
}
// withLock runs f while holding af.mtx, and reports any error it returns.
func (af *AutoFile) withLock(f func() error) error {
af.mtx.Lock()
defer af.mtx.Unlock()
return f()
}
// Write writes len(b) bytes to the AutoFile. It returns the number of bytes
// written and an error, if any. Write returns a non-nil error when n !=
// len(b).
// Opens AutoFile if needed.
func (af *AutoFile) Write(b []byte) (n int, err error) {
af.mtx.Lock()
defer af.mtx.Unlock()
if af.closed {
return 0, fmt.Errorf("write: %w", errAutoFileClosed)
}
if af.file == nil {
if err = af.openFile(); err != nil {
return
}
}
n, err = af.file.Write(b)
return
}
// Sync commits the current contents of the file to stable storage. Typically,
// this means flushing the file system's in-memory copy of recently written
// data to disk.
func (af *AutoFile) Sync() error {
return af.withLock(func() error {
if af.closed {
return fmt.Errorf("sync: %w", errAutoFileClosed)
} else if af.file == nil {
return nil // nothing to sync
}
return af.file.Sync()
})
}
// openFile unconditionally replaces af.file with a new filehandle on the path.
// The caller must hold af.mtx exclusively.
func (af *AutoFile) openFile() error {
file, err := os.OpenFile(af.Path, os.O_RDWR|os.O_CREATE|os.O_APPEND, autoFilePerms)
if err != nil {
return err
}
// fileInfo, err := file.Stat()
// if err != nil {
// return err
// }
// if fileInfo.Mode() != autoFilePerms {
// return errors.NewErrPermissionsChanged(file.Name(), fileInfo.Mode(), autoFilePerms)
// }
af.file = file
return nil
}
// Size returns the size of the AutoFile. It returns -1 and an error if fails
// get stats or open file.
// Opens AutoFile if needed.
func (af *AutoFile) Size() (int64, error) {
af.mtx.Lock()
defer af.mtx.Unlock()
if af.closed {
return 0, fmt.Errorf("size: %w", errAutoFileClosed)
}
if af.file == nil {
if err := af.openFile(); err != nil {
return -1, err
}
}
stat, err := af.file.Stat()
if err != nil {
return -1, err
}
return stat.Size(), nil
}