fix: deadlock when os.Stat errors within rotate method (#1716)

* fix: deadlock when os.Stat errors within rotate method

* pr feedback: adding test
This commit is contained in:
Ramiro 2023-08-10 13:16:58 -07:00 коммит произвёл GitHub
Родитель 34b6c9e9b1
Коммит 2a09dd3a39
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 67 добавлений и 25 удалений

Просмотреть файл

@ -149,42 +149,54 @@ func (logger *Logger) getLogFileName() string {
return logFileName return logFileName
} }
// todo: handle errors and use atomic file rotation // todo: handle errors and use atomic file rotation.
// Rotate checks the active log file size and rotates log files if necessary. // rotate checks the active log file size and rotates log files if necessary.
func (logger *Logger) rotate() { func (logger *Logger) rotate() error {
// Return if target is not a log file. // return if target is not a log file.
if (logger.target != TargetLogfile && logger.target != TargetStdOutAndLogFile) || logger.out == nil { if (logger.target != TargetLogfile && logger.target != TargetStdOutAndLogFile) || logger.out == nil {
return return nil
} }
fileName := logger.getLogFileName() fileName := logger.getLogFileName()
fileInfo, err := os.Stat(fileName) fileInfo, err := os.Stat(fileName)
if err != nil { if err != nil {
logger.Logf("[log] Failed to query log file info %+v.", err) return fmt.Errorf("rotate: could not get file info: %w", err)
return
} }
// Rotate if size limit is reached. // only rotate if size limit is reached.
if fileInfo.Size() >= int64(logger.maxFileSize) { if fileInfo.Size() < int64(logger.maxFileSize) {
logger.out.Close() return nil
var fn1, fn2 string }
// Rotate log files, keeping the last maxFileCount files. // todo: what should happen if current file cannot be closed?
for n := logger.maxFileCount - 1; n >= 0; n-- { _ = logger.out.Close()
fn2 = fn1
if n == 0 { var fn1 string
fn1 = fileName // rotate log files, keeping the last maxFileCount files.
} else { for n := logger.maxFileCount - 1; n >= 0; n-- {
fn1 = fmt.Sprintf("%v.%v", fileName, n) fn2 := fn1
}
if fn2 != "" { if n == 0 {
os.Rename(fn1, fn2) fn1 = fileName
} } else {
fn1 = fmt.Sprintf("%v.%v", fileName, n)
} }
// Create a new log file. if fn2 != "" {
logger.SetTarget(logger.target) // this algorithm attempts renaming non-existent files.
// todo: handle different types of error: if a non-existing file
// is being renamed, then continue. if an existing file cannot
// be renamed, log.
_ = os.Rename(fn1, fn2)
}
} }
// Create a new log file.
if err := logger.SetTarget(logger.target); err != nil {
return fmt.Errorf("rotate: could not set target: %w", err)
}
return nil
} }
// Request logs a structured request. // Request logs a structured request.
@ -221,7 +233,9 @@ func (logger *Logger) ResponseEx(tag string, request interface{}, response inter
// logf logs a formatted string. // logf logs a formatted string.
func (logger *Logger) logf(format string, args ...interface{}) { func (logger *Logger) logf(format string, args ...interface{}) {
if logger.callCount%rotationCheckFrq == 0 { if logger.callCount%rotationCheckFrq == 0 {
logger.rotate() if err := logger.rotate(); err != nil {
logger.l.Printf("logf: could not rotate file: %v", err)
}
} }
format = fmt.Sprintf("[%v] %s", pid, format) format = fmt.Sprintf("[%v] %s", pid, format)
logger.callCount++ logger.callCount++

Просмотреть файл

@ -9,6 +9,9 @@ import (
"path" "path"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const ( const (
@ -42,6 +45,31 @@ func TestNewLoggerError(t *testing.T) {
} }
} }
func TestRotateFailure(t *testing.T) {
// this test simulates a scenario where log rotation should fail because the file to rotate does not exist.
// previously, such a scenario would recursively call Logf, which would deadlock because of the mutexes taken.
// the logging api does not bubble up any errors, and does not allow any configuration of the frequency of
// rotation checks, but we shouldn't couple this test (too much) to the internal algorithm.
//
// the assertions below should demonstrate that:
// - logging does not block even if the target file is missing.
// - an explicit rotation call should return with an error and not block indefinitely.
// - successive calls after a rotation failure should not block either.
l := NewLogger(logName, LevelInfo, TargetStdOutAndLogFile, "/tmp/")
require.NotNil(t, l)
err := os.Remove(l.getLogFileName())
require.NoError(t, err)
l.Logf("this log line may or may not invoke rotation")
err = l.rotate()
var pErr *os.PathError
assert.ErrorAs(t, err, &pErr)
l.Logf("this log line may or may not invoke rotation")
}
// Tests that the log file rotates when size limit is reached. // Tests that the log file rotates when size limit is reached.
func TestLogFileRotatesWhenSizeLimitIsReached(t *testing.T) { func TestLogFileRotatesWhenSizeLimitIsReached(t *testing.T) {
logDirectory := "" // This sets the current location for logs logDirectory := "" // This sets the current location for logs