Files
logwisp/internal/sink/tcpchain/tcpchain_test.go
T
lixen 4af072f4e5
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
v0.25.0 netacl and fixes
2026-10-04 23:43:52 -04:00

140 lines
3.8 KiB
Go

package tcpchain
import (
"bufio"
"fmt"
"net"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/lixenwraith/logwisp/internal/core"
"github.com/lixenwraith/logwisp/internal/session"
"github.com/lixenwraith/log"
)
// A source that refuses each link right after the hello, as a scram source
// does to a sink without credentials, is retried with growing delays, not in
// a tight loop that logs an established link every round.
func TestRefusingSourceIsRetriedWithBackoff(t *testing.T) {
ln, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
var accepts atomic.Int64
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
accepts.Add(1)
bufio.NewReader(conn).ReadString('\n')
conn.Write([]byte(`{"error":"authentication required"}` + "\n"))
conn.Close()
}
}()
_, port, _ := net.SplitHostPort(ln.Addr().String())
p, _ := strconv.ParseInt(port, 10, 64)
manager := session.NewManager(time.Hour)
t.Cleanup(manager.Stop)
created, err := NewTCPChainSinkPlugin("fwd", map[string]any{
"host": "127.0.0.1", "port": p, "backoff_min_ms": int64(20), "backoff_max_ms": int64(5000),
}, log.NewLogger(), session.NewProxy(manager, "fwd"))
if err != nil {
t.Fatal(err)
}
sink := created.(*TCPChainSink)
if err := sink.Start(t.Context()); err != nil {
t.Fatal(err)
}
t.Cleanup(sink.Stop)
deadline := time.Now().Add(1500 * time.Millisecond)
for time.Now().Before(deadline) {
select {
case sink.Input() <- core.TransportEvent{Time: time.Now(), Payload: []byte("entry")}:
default:
}
time.Sleep(5 * time.Millisecond)
}
// 20 ms doubling reaches 1.5 s in about 7 attempts; a constant delay
// would take dozens
if n := accepts.Load(); n > 10 {
t.Fatalf("%d connections in 1.5 s against a refusing source", n)
}
}
// Stop delivers what is queued, so the end of a finite input reaches the
// source whole, and returns at the flush bound when the source stops reading.
func TestStopDeliversTheQueueWithinTheBound(t *testing.T) {
const n = 2000 // of 8 KiB: more than a stalled source's socket buffers hold
padding := strings.Repeat("x", 8192)
manager := session.NewManager(time.Hour)
t.Cleanup(manager.Stop)
// stop queues n entries, starts and stops a sink, and returns how long
// Stop took and how many entries a reading source received
stop := func(reading bool) (time.Duration, int) {
ln, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
lines := make(chan int, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
if !reading {
<-t.Context().Done()
return
}
count, scanner := -1, bufio.NewScanner(conn) // -1: the hello
scanner.Buffer(nil, 64*1024)
for scanner.Scan() {
count++
}
lines <- count
}()
_, port, _ := net.SplitHostPort(ln.Addr().String())
p, _ := strconv.ParseInt(port, 10, 64)
created, err := NewTCPChainSinkPlugin("fwd", map[string]any{
"host": "127.0.0.1", "port": p, "buffer_size": int64(n), // writes time out at 5 s
}, log.NewLogger(), session.NewProxy(manager, "fwd"))
if err != nil {
t.Fatal(err)
}
sink := created.(*TCPChainSink)
for i := range n {
sink.Input() <- core.TransportEvent{Time: time.Now(), Payload: fmt.Appendf(nil, "entry %d %s", i, padding)}
}
if err := sink.Start(t.Context()); err != nil {
t.Fatal(err)
}
start := time.Now()
sink.Stop()
d := time.Since(start)
if !reading {
return d, 0
}
select {
case got := <-lines:
return d, got
case <-time.After(5 * time.Second):
return d, 0
}
}
if _, got := stop(true); got != n {
t.Fatalf("the source received %d of %d entries", got, n)
}
if d, _ := stop(false); d > 3500*time.Millisecond {
t.Fatalf("Stop took %v past a 2 s flush bound", d)
}
}