Repository navigation
Conversation
The rotating log writer starts a goroutine that reads from an io.Pipe, but its Write method bypasses the pipe and accesses the rotator directly. Concurrent subsystem loggers can therefore race with each other and with the reader goroutine over the rotator's file and size state. In this commit, we send writes through the existing pipe so its reader is the sole owner of the rotator. Close now shuts down the pipe, waits for the reader, and returns any runtime rotation or close error.
🟡 PR Severity: MEDIUM
🟡 Medium (1 file)
🟢 Low (1 file)
AnalysisThe only production code change is in To override, add a |
Lrifton92
left a comment
There was a problem hiding this comment.
Read this against jrick/logrotate v1.1.2 (the version pinned in go.mod) and the change is sound: Run is now the only goroutine touching the rotator, Rotator.Close waits on the compress WaitGroup, and closing the pipe first, then draining runErr, then closing the rotator is the right order. No deadlock if Run already exited with an error, since runErr is buffered and CloseWithError unblocks any writer.
Two things worth a look before merge:
-
Runtime rotation errors become invisible until shutdown. Before this PR a failed
rotate()(disk full, rename refused) printedfailed to run file rotatorto stderr once. Now the error only lives in the pipe: every laterWritereturns it, but thebtcloghandlers drop write errors, so file logging stops silently for the rest of the process lifetime. The error resurfaces only in the deferredcfg.LogRotator.Close()inlnd.go(ltndLog.Errorf("Could not close log rotator")), and at that point the file handler is already closed, so it is only visible on the console handler at exit. Suggest keeping the one-shot stderr line in the goroutine whenerr != nil(after theio.EOFnormalisation), in addition toCloseWithError. -
Test constant coupling.
testLogFileSize = 1000 * 1024only triggers the rotation because logrotate defines its threshold as1000 * thresholdKBbytes (rotator.go:79) whileInitLogRotatorpassesMaxLogFileSize*1024. WithMaxLogFileSize = 1the threshold is exactly 1,024,000 bytes, so the second 512,000-byte line crosses it and produces exactly one.1.gz. It works, but a one-line comment on the constant (or deriving it aslogConfig.MaxLogFileSize * 1024 * 1000) would save the next reader a detour into the dependency.
Minor: the errors.Is(err, io.EOF) normalisation is required, Run returns ReadLine's io.EOF verbatim when the pipe is closed. Moving the compressor check ahead of MkdirAll/rotator.New is a nice side effect too, no dangling open file on an unknown compressor.
|
@Roasbeef, remember to re-request review from reviewers when ready |
4 similar comments
|
@Roasbeef, remember to re-request review from reviewers when ready |
|
@Roasbeef, remember to re-request review from reviewers when ready |
|
@Roasbeef, remember to re-request review from reviewers when ready |
|
@Roasbeef, remember to re-request review from reviewers when ready |
Overview
The rotating log writer starts a goroutine that consumes an
io.Pipe, butits
Writemethod currently bypasses the pipe and writes to the underlyingrotator directly. Since subsystem loggers share this writer, concurrent log
calls can race over the rotator's file and size state. The direct write can
also race with the reader goroutine during startup.
This PR routes writes through the existing pipe. The reader goroutine becomes
the sole owner of the rotator, while
io.Pipeserializes concurrent writers.Shutdown now closes the pipe, waits for the reader, closes the rotator, and
returns any errors from those stages. Repeated
Closecalls are idempotent.Testing
go test -race ./buildgo test -race ./build -run TestRotatingLogWriterConcurrentWrites -count=20make fmt-checkgo vet ./buildThe regression test writes concurrently across the rotation threshold, waits
for shutdown, and decompresses the resulting gzip backup through EOF.