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:
Родитель
34b6c9e9b1
Коммит
2a09dd3a39
|
@ -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
|
||||||
|
|
Загрузка…
Ссылка в новой задаче