mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-26 01:44:16 +00:00
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.
225 lines
5.2 KiB
Go
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
|
|
}
|