Skip to content

Commit 5546c45

Browse files
vitkyrkaclaude
andcommitted
fix(logs): refresh symlinkPolicy in Tailer.ReplaceSource
When the launcher reuses an existing tailer for a new source (same scan key, different source object), it calls ReplaceSource(). Previously the tailer's symlinkPolicy was frozen at construction time, so swapping a NoFollow=true source for a NoFollow=false source (or vice-versa) left the open policy stale — subsequent reopens and rotation checks used the policy from the original source. Fix by updating symlinkPolicy from the new source's NoFollow flag inside ReplaceSource(), and add a unit test that covers all three transitions (false→true→false). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 82cc797 commit 5546c45

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

pkg/logs/tailers/file/tailer.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,15 @@ func (t *Tailer) recordBytes(n int64) {
479479
t.bytesRead.Add(n)
480480
}
481481

482-
// ReplaceSource replaces the current source
482+
// ReplaceSource replaces the current source and refreshes open-policy state
483+
// derived from the source config (e.g. NoFollow).
483484
func (t *Tailer) ReplaceSource(newSource *sources.LogSource) {
484485
t.file.Source.Replace(newSource)
486+
if cfg := newSource.Config; cfg != nil && cfg.NoFollow {
487+
t.symlinkPolicy = opener.RejectSymlinks
488+
} else {
489+
t.symlinkPolicy = opener.FollowSymlinks
490+
}
485491
}
486492

487493
// Source gets the source (currently only used for testing)

pkg/logs/tailers/file/tailer_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,3 +680,54 @@ func TestNoGoLeakWithNonBlockingStop(t *testing.T) {
680680

681681
// The deferred goleak.VerifyNone() will detect if goroutine leaked
682682
}
683+
684+
// TestReplaceSourceRefreshesSymlinkPolicy verifies that ReplaceSource updates
685+
// the tailer's symlinkPolicy to match the new source's NoFollow flag.
686+
func TestReplaceSourceRefreshesSymlinkPolicy(t *testing.T) {
687+
testDir := t.TempDir()
688+
testPath := filepath.Join(testDir, "tailer.log")
689+
f, err := os.Create(testPath)
690+
if err != nil {
691+
t.Fatal(err)
692+
}
693+
defer f.Close()
694+
695+
info := status.NewInfoRegistry()
696+
makeSource := func(noFollow bool) *sources.LogSource {
697+
return sources.NewLogSource("", &config.LogsConfig{
698+
Type: config.FileType,
699+
Path: testPath,
700+
NoFollow: noFollow,
701+
})
702+
}
703+
704+
tailerOptions := &TailerOptions{
705+
OutputChan: make(chan *message.Message, chanSize),
706+
File: NewFile(testPath, makeSource(false), false),
707+
SleepDuration: 10 * time.Millisecond,
708+
Decoder: decoder.NewDecoderFromSource(sources.NewReplaceableSource(makeSource(false)), info),
709+
Info: info,
710+
CapacityMonitor: metrics.NewNoopPipelineMonitor("").GetCapacityMonitor("", ""),
711+
Registry: auditor.NewMockRegistry(),
712+
FileOpener: opener.NewFileOpener(),
713+
}
714+
715+
tailer := NewTailer(tailerOptions)
716+
717+
// Initial state: NoFollow=false → FollowSymlinks.
718+
if tailer.symlinkPolicy != opener.FollowSymlinks {
719+
t.Fatalf("expected FollowSymlinks initially, got %v", tailer.symlinkPolicy)
720+
}
721+
722+
// Replace with NoFollow=true → policy must become RejectSymlinks.
723+
tailer.ReplaceSource(makeSource(true))
724+
if tailer.symlinkPolicy != opener.RejectSymlinks {
725+
t.Fatalf("expected RejectSymlinks after NoFollow=true source, got %v", tailer.symlinkPolicy)
726+
}
727+
728+
// Replace again with NoFollow=false → policy must revert to FollowSymlinks.
729+
tailer.ReplaceSource(makeSource(false))
730+
if tailer.symlinkPolicy != opener.FollowSymlinks {
731+
t.Fatalf("expected FollowSymlinks after NoFollow=false source, got %v", tailer.symlinkPolicy)
732+
}
733+
}

0 commit comments

Comments
 (0)