Skip to content

Commit

Permalink
StopAtEOF: keep sending lines until EOF
Browse files Browse the repository at this point in the history
When a StopAtEOF() is called the code should continue to send all lines
to the Lines channel. The issue here is if the caller is not ready to
receive a new line the code blocks as it is using a unbuffered channel.
However <-tail.Dying() would return in this case so the line was
skipped. This means that the caller did not get all lines until EOF.
Now we still want to skip in case any other reason for kill was given
therefore add special logic to only not read the Dying channel on the
EOF case.

The one downside is that StopAtEOF() could block forever if the caller
never reads new Lines but this seems logical to me. If the caller wants
to wait for EOF but never reads remaining Lines this would be a bug on
their end.

Fixes nxadm#37

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
  • Loading branch information
Luap99 committed Jun 26, 2024
1 parent ba755e4 commit ad4e60e
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 8 deletions.
25 changes: 19 additions & 6 deletions tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.

//nxadm/tail provides a Go library that emulates the features of the BSD `tail`
//program. The library comes with full support for truncation/move detection as
//it is designed to work with log rotation tools. The library works on all
//operating systems supported by Go, including POSIX systems like Linux and
//*BSD, and MS Windows. Go 1.9 is the oldest compiler release supported.
// nxadm/tail provides a Go library that emulates the features of the BSD `tail`
// program. The library comes with full support for truncation/move detection as
// it is designed to work with log rotation tools. The library works on all
// operating systems supported by Go, including POSIX systems like Linux and
// *BSD, and MS Windows. Go 1.9 is the oldest compiler release supported.
package tail

import (
Expand Down Expand Up @@ -450,12 +450,25 @@ func (tail *Tail) sendLine(line string) bool {
lines = util.PartitionString(line, tail.MaxLineSize)
}

// This is a bit weird here, when a users requests stopAtEof we
// must keep sending all lines however <-tail.Dying() will return
// immediately at this point so the select below may not have
// chance to send the line if the reader side has is not yet ready.
// But if StopAtEOF was not set and it is a "normal" Kill then we
// should exit right away still thus the special logic here.
earlyExitChan := tail.Dying()
if tail.Err() == errStopAtEOF {
// Note that receive from a nil channel blocks forever so
// below we know it can only take the tail.Lines case.
earlyExitChan = nil
}

for _, line := range lines {
tail.lineNum++
offset, _ := tail.Tell()
select {
case tail.Lines <- &Line{line, tail.lineNum, SeekInfo{Offset: offset}, now, nil}:
case <-tail.Dying():
case <-earlyExitChan:
return true
}
}
Expand Down
32 changes: 30 additions & 2 deletions tail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,26 @@ func TestIncompleteLinesWithoutFollow(t *testing.T) {
tail.Cleanup()
}

func TestFollowUntilEof(t *testing.T) {
tailTest, cleanup := NewTailTest("incomplete-lines-no-follow", t)
defer cleanup()
filename := "test.txt"
config := Config{
Follow: false,
}
tailTest.CreateFile(filename, "hello\nworld\n")
tail := tailTest.StartTail(filename, config)

// StopAtEOF blocks until the read is done and in order to do so
// we have to drain the lines channel first which ReadLinesWithError does.
go tail.StopAtEOF()
tailTest.ReadLinesWithError(tail, []string{"hello", "world"}, false, errStopAtEOF)

tailTest.RemoveFile(filename)
tail.Stop()
tail.Cleanup()
}

func reSeek(t *testing.T, poll bool) {
var name string
if poll {
Expand Down Expand Up @@ -765,6 +785,14 @@ func (t TailTest) VerifyTailOutputUsingCursor(tail *Tail, lines []string, expect
}

func (t TailTest) ReadLines(tail *Tail, lines []string, useCursor bool) {
t.readLines(tail, lines, useCursor, nil)
}

func (t TailTest) ReadLinesWithError(tail *Tail, lines []string, useCursor bool, err error) {
t.readLines(tail, lines, useCursor, err)
}

func (t TailTest) readLines(tail *Tail, lines []string, useCursor bool, expectErr error) {
cursor := 1

for _, line := range lines {
Expand All @@ -773,8 +801,8 @@ func (t TailTest) ReadLines(tail *Tail, lines []string, useCursor bool) {
if !ok {
// tail.Lines is closed and empty.
err := tail.Err()
if err != nil {
t.Fatalf("tail ended with error: %v", err)
if err != expectErr {
t.Fatalf("tail ended with unexpected error: %v", err)
}
t.Fatalf("tail ended early; expecting more: %v", lines[cursor:])
}
Expand Down

0 comments on commit ad4e60e

Please sign in to comment.