Skip to content

Commit b77c1eb

Browse files
committed
fix(logwatchers/kmsg): don't block Stop() when the log channel is full
The log monitor stops draining logCh before calling watcher.Stop(), so with a full channel (e.g. a kmsg burst at shutdown) watchLoop blocked on the send forever, never called tomb.Done(), and Stop() hung. Select on tomb.Stopping() alongside the send.
1 parent 187d4d3 commit b77c1eb

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

pkg/systemlogmonitor/logwatchers/kmsg/log_watcher_linux.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,16 @@ func (k *kernelLogWatcher) watchLoop() {
135135
continue
136136
}
137137

138-
k.logCh <- &logtypes.Log{
138+
// The consumer stops draining logCh before calling Stop(), so a
139+
// plain send on a full channel could block forever and deadlock Stop().
140+
select {
141+
case k.logCh <- &logtypes.Log{
139142
Message: strings.TrimSpace(msg.Message),
140143
Timestamp: msg.Timestamp,
144+
}:
145+
case <-k.tomb.Stopping():
146+
klog.Infof("Stop watching kernel log")
147+
return
141148
}
142149
}
143150
}

pkg/systemlogmonitor/logwatchers/kmsg/log_watcher_linux_test.go

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

471+
// TestStopDoesNotDeadlockWhenLogChannelFull verifies that Stop() returns even
472+
// when logCh is full and nobody is draining it.
473+
func TestStopDoesNotDeadlockWhenLogChannelFull(t *testing.T) {
474+
now := time.Now()
475+
476+
// More messages than logCh capacity so watchLoop ends up blocked sending.
477+
kmsgs := make([]kmsgparser.Message, 150)
478+
for i := range kmsgs {
479+
kmsgs[i] = kmsgparser.Message{Message: fmt.Sprintf("msg-%d", i), Timestamp: now}
480+
}
481+
482+
w := &kernelLogWatcher{
483+
cfg: types.WatcherConfig{},
484+
startTime: now.Add(-time.Minute),
485+
tomb: tomb.NewTomb(),
486+
logCh: make(chan *logtypes.Log, 100),
487+
kmsgParser: &mockKmsgParser{kmsgs: kmsgs},
488+
}
489+
490+
// Watch but never read logCh, mimicking the log monitor after it has
491+
// decided to stop.
492+
_, err := w.Watch()
493+
assert.NoError(t, err)
494+
495+
// Let watchLoop fill the channel and block on the send.
496+
time.Sleep(300 * time.Millisecond)
497+
498+
stopped := make(chan struct{})
499+
go func() {
500+
w.Stop()
501+
close(stopped)
502+
}()
503+
select {
504+
case <-stopped:
505+
case <-time.After(2 * time.Second):
506+
t.Fatal("Stop() deadlocked while logCh was full")
507+
}
508+
}
509+
471510
// TestWatcherProcessesMessageContent verifies watchLoop's per-message
472511
// handling: empty messages are dropped, and surrounding whitespace is
473512
// trimmed before forwarding.

0 commit comments

Comments
 (0)