@@ -19,6 +19,7 @@ package kmsg
1919import (
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