summaryrefslogtreecommitdiff
path: root/internal/server/handlers/readcommand_epoch_order_test.go
blob: fb85fe850c23f2c6ec28e6e0c2505d0938f17655 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package handlers

// Test pinning the load-bearing call ordering in readFiles' output EOF
// epilogue: the handshake epoch (OutputEpoch) MUST be captured before the
// pending-work check (PendingAndActive). Joiners increment the pending count
// before enabling output mode, so capturing the epoch first guarantees that a
// joiner invisible to the pending==0 check bumps the epoch after the capture,
// turning the stale SignalOutputEOF into a no-op. Reordering the two calls
// would silently reopen the mid-batch EOF window; this test fails if someone
// does that.

import (
	"context"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/mimecast/dtail/internal/lcontext"
	"github.com/mimecast/dtail/internal/omode"
	"github.com/mimecast/dtail/internal/regex"
)

// epochOrderTestServer wraps globCapTestServer and records the order of the
// output-handshake-relevant calls made by readFiles. It reports an enabled
// output session (DirectOutputActive true, HasOutputEOF
// true) so readFiles runs the full EOF epilogue.
type epochOrderTestServer struct {
	*globCapTestServer

	mu             sync.Mutex
	calls          []string
	signaledEpochs []uint64
	outputLines     chan []byte
}

func newEpochOrderTestServer() *epochOrderTestServer {
	return &epochOrderTestServer{
		globCapTestServer: newGlobCapTestServer(100),
		outputLines:        make(chan []byte, 100),
	}
}

func (s *epochOrderTestServer) record(name string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.calls = append(s.calls, name)
}

func (s *epochOrderTestServer) recordedCalls() []string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return append([]string(nil), s.calls...)
}

func (s *epochOrderTestServer) DirectOutputActive() bool        { return true }
func (s *epochOrderTestServer) EnableDirectOutput() bool    { return false }
func (s *epochOrderTestServer) HasOutputEOF() bool        { return true }

func (s *epochOrderTestServer) GetOutputChannel() chan []byte { return s.outputLines }

// OutputEpoch returns a sentinel value so the test can also verify that the
// captured epoch is the one passed through to SignalOutputEOF.
func (s *epochOrderTestServer) OutputEpoch() uint64 {
	s.record("OutputEpoch")
	return 42
}

func (s *epochOrderTestServer) PendingAndActive() (int32, int32) {
	s.record("PendingAndActive")
	return s.globCapTestServer.PendingAndActive()
}

func (s *epochOrderTestServer) FlushOutput() {
	s.record("FlushOutput")
}

func (s *epochOrderTestServer) SignalOutputEOF(epoch uint64) {
	s.mu.Lock()
	s.signaledEpochs = append(s.signaledEpochs, epoch)
	s.mu.Unlock()
	s.record("SignalOutputEOF")
}

var _ readCommandServer = (*epochOrderTestServer)(nil)

// TestReadFilesCapturesEpochBeforePendingCheck drives readFiles over a real
// (empty) file with an enabled output session and asserts that the EOF
// epilogue runs exactly OutputEpoch -> PendingAndActive -> FlushOutput ->
// SignalOutputEOF, i.e. the epoch is captured before the pending check.
// Earlier PendingAndActive calls from the per-file phase are recorded too,
// which is why the assertion checks the trailing four calls.
func TestReadFilesCapturesEpochBeforePendingCheck(t *testing.T) {
	resetServerLogger(t)

	dir := t.TempDir()
	path := filepath.Join(dir, "empty.log")
	if err := os.WriteFile(path, nil, 0o600); err != nil {
		t.Fatalf("create temp file: %v", err)
	}

	srv := newEpochOrderTestServer()
	cmd := newReadCommand(srv, omode.CatClient)
	cmd.readFiles(context.Background(), lcontext.LContext{}, []string{path}, path,
		regex.NewNoop(), time.Millisecond)

	calls := srv.recordedCalls()
	wantTail := []string{"OutputEpoch", "PendingAndActive", "FlushOutput", "SignalOutputEOF"}
	if len(calls) < len(wantTail) {
		t.Fatalf("EOF epilogue did not run, recorded calls: %v", calls)
	}
	tail := calls[len(calls)-len(wantTail):]
	for i, want := range wantTail {
		if tail[i] != want {
			t.Fatalf("EOF epilogue call order = %v, want trailing %v (epoch must be captured BEFORE the pending check)",
				tail, wantTail)
		}
	}

	srv.mu.Lock()
	signaled := append([]uint64(nil), srv.signaledEpochs...)
	srv.mu.Unlock()
	if len(signaled) != 1 || signaled[0] != 42 {
		t.Fatalf("SignalOutputEOF epochs = %v, want exactly [42] (the captured OutputEpoch value)", signaled)
	}
}