Skip to content

Commit a20306e

Browse files
committed
fix(logwatchers/kmsg): don't close the old parser twice when Stop() interrupts a restart
The restart path closes the failed parser before retrying. If stopping is signaled during the retry wait, watchLoop's deferred cleanup closed the same parser again, logging a spurious 'file already closed' error at shutdown. Clear the reference after closing and nil-check the defer.
1 parent b77c1eb commit a20306e

2 files changed

Lines changed: 61 additions & 3 deletions

File tree

pkg/systemlogmonitor/logwatchers/kmsg/log_watcher_linux.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,11 @@ func (k *kernelLogWatcher) Stop() {
9494
func (k *kernelLogWatcher) watchLoop() {
9595
kmsgs := k.kmsgParser.Parse()
9696
defer func() {
97-
if err := k.kmsgParser.Close(); err != nil {
98-
klog.Errorf("Failed to close kmsg parser: %v", err)
97+
// kmsgParser is nil when stopping interrupted a restart.
98+
if k.kmsgParser != nil {
99+
if err := k.kmsgParser.Close(); err != nil {
100+
klog.Errorf("Failed to close kmsg parser: %v", err)
101+
}
99102
}
100103
close(k.logCh)
101104
k.tomb.Done()
@@ -110,10 +113,12 @@ func (k *kernelLogWatcher) watchLoop() {
110113
if !ok {
111114
klog.Error("Kmsg channel closed, attempting to restart kmsg parser")
112115

113-
// Close the old parser
116+
// Close the old parser and clear the reference so the
117+
// deferred cleanup doesn't close it a second time.
114118
if err := k.kmsgParser.Close(); err != nil {
115119
klog.Errorf("Failed to close kmsg parser: %v", err)
116120
}
121+
k.kmsgParser = nil
117122

118123
// Try to restart. retryCreateParser() waits between attempts.
119124
var restarted bool

pkg/systemlogmonitor/logwatchers/kmsg/log_watcher_linux_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,59 @@ func TestWatcherRateLimitsRestarts(t *testing.T) {
468468
}
469469
}
470470

471+
// TestStopDuringRestartClosesOldParserOnce verifies that when Stop() arrives
472+
// while the watcher is in the restart path, the already-closed old parser is
473+
// not closed a second time by watchLoop's deferred cleanup.
474+
func TestStopDuringRestartClosesOldParserOnce(t *testing.T) {
475+
now := time.Now()
476+
477+
// Closing the channel after sending drives watchLoop into the restart path.
478+
mock := &mockKmsgParser{
479+
kmsgs: []kmsgparser.Message{{Message: "msg", Timestamp: now}},
480+
closeAfterSend: true,
481+
}
482+
483+
factoryCalled := make(chan struct{}, 1)
484+
w := &kernelLogWatcher{
485+
cfg: types.WatcherConfig{},
486+
startTime: now.Add(-time.Minute),
487+
tomb: tomb.NewTomb(),
488+
logCh: make(chan *logtypes.Log, 100),
489+
kmsgParser: mock,
490+
newParser: func() (kmsgparser.Parser, error) {
491+
select {
492+
case factoryCalled <- struct{}{}:
493+
default:
494+
}
495+
// Keep the watcher in the retry loop until Stop() is called.
496+
return nil, fmt.Errorf("kmsg unavailable")
497+
},
498+
}
499+
500+
logCh, err := w.Watch()
501+
assert.NoError(t, err)
502+
<-logCh
503+
504+
// Wait until watchLoop has entered the restart path.
505+
select {
506+
case <-factoryCalled:
507+
case <-time.After(time.Second):
508+
t.Fatal("timeout waiting for restart attempt")
509+
}
510+
511+
w.Stop()
512+
513+
select {
514+
case _, ok := <-logCh:
515+
assert.False(t, ok, "log channel should be closed after Stop()")
516+
case <-time.After(time.Second):
517+
t.Fatal("timeout waiting for log channel to close after Stop()")
518+
}
519+
520+
assert.Equal(t, 1, mock.CloseCallCount(),
521+
"old parser must be closed exactly once, not again by watchLoop's defer")
522+
}
523+
471524
// TestStopDoesNotDeadlockWhenLogChannelFull verifies that Stop() returns even
472525
// when logCh is full and nobody is draining it.
473526
func TestStopDoesNotDeadlockWhenLogChannelFull(t *testing.T) {

0 commit comments

Comments
 (0)