Skip to content

Commit 1a2203f

Browse files
Merge pull request #1311 from hakman/kmsg-watcher-fixes
Fix kmsg watcher restart and shutdown issues
2 parents 97d58d2 + a20306e commit 1a2203f

2 files changed

Lines changed: 174 additions & 6 deletions

File tree

pkg/systemlogmonitor/logwatchers/kmsg/log_watcher_linux.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ type kernelLogWatcher struct {
4444
kmsgParser kmsgparser.Parser
4545
// newParser creates a kmsgparser. Overridable in tests; defaults to kmsgparser.NewParser.
4646
newParser func() (kmsgparser.Parser, error)
47+
// lastRestart is when the parser was last restarted; used to rate-limit restarts.
48+
lastRestart time.Time
4749
}
4850

4951
// NewKmsgWatcher creates a watcher which will read messages from /dev/kmsg
@@ -92,8 +94,11 @@ func (k *kernelLogWatcher) Stop() {
9294
func (k *kernelLogWatcher) watchLoop() {
9395
kmsgs := k.kmsgParser.Parse()
9496
defer func() {
95-
if err := k.kmsgParser.Close(); err != nil {
96-
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+
}
97102
}
98103
close(k.logCh)
99104
k.tomb.Done()
@@ -108,13 +113,14 @@ func (k *kernelLogWatcher) watchLoop() {
108113
if !ok {
109114
klog.Error("Kmsg channel closed, attempting to restart kmsg parser")
110115

111-
// 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.
112118
if err := k.kmsgParser.Close(); err != nil {
113119
klog.Errorf("Failed to close kmsg parser: %v", err)
114120
}
121+
k.kmsgParser = nil
115122

116-
// Try to restart immediately. retryCreateParser() applies backoff only
117-
// after a failed NewParser() or SeekEnd() attempt.
123+
// Try to restart. retryCreateParser() waits between attempts.
118124
var restarted bool
119125
kmsgs, restarted = k.retryCreateParser()
120126
if !restarted {
@@ -134,16 +140,26 @@ func (k *kernelLogWatcher) watchLoop() {
134140
continue
135141
}
136142

137-
k.logCh <- &logtypes.Log{
143+
// The consumer stops draining logCh before calling Stop(), so a
144+
// plain send on a full channel could block forever and deadlock Stop().
145+
select {
146+
case k.logCh <- &logtypes.Log{
138147
Message: strings.TrimSpace(msg.Message),
139148
Timestamp: msg.Timestamp,
149+
}:
150+
case <-k.tomb.Stopping():
151+
klog.Infof("Stop watching kernel log")
152+
return
140153
}
141154
}
142155
}
143156
}
144157

145158
// retryCreateParser attempts to create a new kmsg parser.
146159
// It tries immediately first, then waits retryDelay between subsequent failures.
160+
// The first attempt is also delayed if the previous restart was less than
161+
// retryDelay ago, so a parser that keeps failing right after a successful
162+
// restart cannot drive a hot restart loop.
147163
// On success, it seeks the new parser to the end of the kmsg ring buffer to
148164
// avoid replaying messages that were already processed before the restart.
149165
// Any messages written to kmsg between the old parser closing and the new
@@ -152,6 +168,15 @@ func (k *kernelLogWatcher) watchLoop() {
152168
// restart was triggered by a kmsg flood.
153169
// It returns the new message channel and true on success, or nil and false if stopping was signaled.
154170
func (k *kernelLogWatcher) retryCreateParser() (<-chan kmsgparser.Message, bool) {
171+
if since := time.Since(k.lastRestart); since < retryDelay {
172+
select {
173+
case <-k.tomb.Stopping():
174+
klog.Infof("Stop watching kernel log during restart attempt")
175+
return nil, false
176+
case <-time.After(retryDelay - since):
177+
}
178+
}
179+
155180
for {
156181
parser, err := k.newParser()
157182
if err != nil {
@@ -163,6 +188,7 @@ func (k *kernelLogWatcher) retryCreateParser() (<-chan kmsgparser.Message, bool)
163188
}
164189
} else {
165190
k.kmsgParser = parser
191+
k.lastRestart = time.Now()
166192
klog.Infof("Successfully restarted kmsg parser")
167193
return parser.Parse(), true
168194
}

pkg/systemlogmonitor/logwatchers/kmsg/log_watcher_linux_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package kmsg
1919
import (
2020
"fmt"
2121
"sync"
22+
"sync/atomic"
2223
"testing"
2324
"time"
2425

@@ -418,6 +419,147 @@ func TestWatcherRestartsOnUnexpectedChannelClose(t *testing.T) {
418419
assert.Equal(t, 1, second.SeekEndCallCount(), "SeekEnd should be called once on the restart parser")
419420
}
420421

422+
// TestWatcherRateLimitsRestarts verifies that a parser whose channel closes
423+
// right after every restart does not drive a hot restart loop: only the first
424+
// restart is immediate, the next attempt waits retryDelay.
425+
func TestWatcherRateLimitsRestarts(t *testing.T) {
426+
now := time.Now()
427+
428+
var factoryCalls int64
429+
w := &kernelLogWatcher{
430+
cfg: types.WatcherConfig{},
431+
startTime: now.Add(-time.Minute),
432+
tomb: tomb.NewTomb(),
433+
logCh: make(chan *logtypes.Log, 100),
434+
// Every parser closes its channel immediately, triggering restarts.
435+
kmsgParser: &mockKmsgParser{closeAfterSend: true},
436+
newParser: func() (kmsgparser.Parser, error) {
437+
atomic.AddInt64(&factoryCalls, 1)
438+
return &mockKmsgParser{closeAfterSend: true}, nil
439+
},
440+
}
441+
442+
logCh, err := w.Watch()
443+
assert.NoError(t, err)
444+
445+
// The first restart is immediate; the second must wait retryDelay (5s),
446+
// so within this window the factory must be called exactly once.
447+
time.Sleep(500 * time.Millisecond)
448+
assert.EqualValues(t, 1, atomic.LoadInt64(&factoryCalls),
449+
"only one restart should happen within retryDelay")
450+
451+
// Stop() must return promptly while the watcher waits out the rate limit.
452+
stopped := make(chan struct{})
453+
go func() {
454+
w.Stop()
455+
close(stopped)
456+
}()
457+
select {
458+
case <-stopped:
459+
case <-time.After(time.Second):
460+
t.Fatal("timeout waiting for Stop() during restart rate-limit wait")
461+
}
462+
463+
select {
464+
case _, ok := <-logCh:
465+
assert.False(t, ok, "log channel should be closed after Stop()")
466+
case <-time.After(time.Second):
467+
t.Fatal("timeout waiting for log channel to close after Stop()")
468+
}
469+
}
470+
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+
524+
// TestStopDoesNotDeadlockWhenLogChannelFull verifies that Stop() returns even
525+
// when logCh is full and nobody is draining it.
526+
func TestStopDoesNotDeadlockWhenLogChannelFull(t *testing.T) {
527+
now := time.Now()
528+
529+
// More messages than logCh capacity so watchLoop ends up blocked sending.
530+
kmsgs := make([]kmsgparser.Message, 150)
531+
for i := range kmsgs {
532+
kmsgs[i] = kmsgparser.Message{Message: fmt.Sprintf("msg-%d", i), Timestamp: now}
533+
}
534+
535+
w := &kernelLogWatcher{
536+
cfg: types.WatcherConfig{},
537+
startTime: now.Add(-time.Minute),
538+
tomb: tomb.NewTomb(),
539+
logCh: make(chan *logtypes.Log, 100),
540+
kmsgParser: &mockKmsgParser{kmsgs: kmsgs},
541+
}
542+
543+
// Watch but never read logCh, mimicking the log monitor after it has
544+
// decided to stop.
545+
_, err := w.Watch()
546+
assert.NoError(t, err)
547+
548+
// Let watchLoop fill the channel and block on the send.
549+
time.Sleep(300 * time.Millisecond)
550+
551+
stopped := make(chan struct{})
552+
go func() {
553+
w.Stop()
554+
close(stopped)
555+
}()
556+
select {
557+
case <-stopped:
558+
case <-time.After(2 * time.Second):
559+
t.Fatal("Stop() deadlocked while logCh was full")
560+
}
561+
}
562+
421563
// TestWatcherProcessesMessageContent verifies watchLoop's per-message
422564
// handling: empty messages are dropped, and surrounding whitespace is
423565
// trimmed before forwarding.

0 commit comments

Comments
 (0)