Files
data-mesher/pkg/net/deadline_conn_test.go
brianmcgee 6c5328a54f feat: protect against slow nodes
Protects against slow or unresponsive nodes during file transfers by enforcing per-operation deadlines and adding an overall timeout for file downloads.

- adds `DeadlineConn` wrapper that resets read/write deadlines after each I/O operation, ensuring data must flow continuously
- replaces `FileClient` with `FileDownloader` - a background download manager that:
    - limits concurrent downloads to 2x CPU count
    - deduplicates in-flight requests for the same file
    - cancels obsolete downloads when a newer file version becomes available
    - enforces an overall download timeout (default 10 minutes)

Closes #286
2026-01-26 11:40:31 +00:00

153 lines
3.6 KiB
Go

package net_test
import (
"net"
"os"
"testing"
"time"
dmnet "git.clan.lol/clan/data-mesher/pkg/net"
"github.com/stretchr/testify/require"
)
func TestDeadlineConn_ReadTimeout(t *testing.T) {
t.Parallel()
as := require.New(t)
// Use net.Pipe for a bidirectional connection
clientConn, serverConn := net.Pipe()
defer func() { _ = clientConn.Close() }()
defer func() { _ = serverConn.Close() }()
// Wrap client with deadline
dc := &dmnet.DeadlineConn{Conn: clientConn, ReadDeadline: 50 * time.Millisecond}
// Try to read - server doesn't send anything, so this should timeout
buf := make([]byte, 100)
_, err := dc.Read(buf)
as.Error(err, "expected timeout error")
as.True(os.IsTimeout(err), "expected timeout error, got: %v", err)
}
func TestDeadlineConn_WriteTimeout(t *testing.T) {
t.Parallel()
as := require.New(t)
// Use net.Pipe for a bidirectional connection
clientConn, serverConn := net.Pipe()
defer func() { _ = clientConn.Close() }()
defer func() { _ = serverConn.Close() }()
// Wrap client with deadline
dc := &dmnet.DeadlineConn{Conn: clientConn, WriteDeadline: 50 * time.Millisecond}
// Write data without reading on the other end - with net.Pipe, writes
// block until the other side reads, so this should timeout
data := make([]byte, 1024*1024) // Large enough to fill any buffer
_, err := dc.Write(data)
as.Error(err, "expected timeout error")
as.True(os.IsTimeout(err), "expected timeout error, got: %v", err)
}
func TestDeadlineConn_SuccessfulReadWrite(t *testing.T) {
t.Parallel()
as := require.New(t)
// Use net.Pipe for a bidirectional connection
clientConn, serverConn := net.Pipe()
defer func() { _ = clientConn.Close() }()
defer func() { _ = serverConn.Close() }()
// Wrap with reasonable timeouts
dc := &dmnet.DeadlineConn{Conn: clientConn, ReadDeadline: time.Second, WriteDeadline: time.Second}
testData := []byte("hello world")
// Write in a goroutine
errCh := make(chan error, 1)
go func() {
_, err := dc.Write(testData)
errCh <- err
}()
// Read from server side
buf := make([]byte, len(testData))
n, err := serverConn.Read(buf)
as.NoError(err)
as.Equal(len(testData), n)
as.Equal(testData, buf)
// Check goroutine completed without error
as.NoError(<-errCh)
}
func TestDeadlineConn_ZeroTimeoutDisablesDeadline(t *testing.T) {
t.Parallel()
as := require.New(t)
clientConn, serverConn := net.Pipe()
defer func() { _ = clientConn.Close() }()
defer func() { _ = serverConn.Close() }()
// Create with zero timeout (should not set deadline)
dc := &dmnet.DeadlineConn{Conn: clientConn}
testData := []byte("test")
// This should work fine - write data and read it
go func() {
_, _ = dc.Write(testData)
}()
buf := make([]byte, len(testData))
n, err := serverConn.Read(buf)
as.NoError(err)
as.Equal(len(testData), n)
}
func TestDeadlineConn_RepeatedReadsResetDeadline(t *testing.T) {
t.Parallel()
as := require.New(t)
clientConn, serverConn := net.Pipe()
defer func() { _ = clientConn.Close() }()
defer func() { _ = serverConn.Close() }()
// Short timeout per read
dc := &dmnet.DeadlineConn{
Conn: clientConn,
ReadDeadline: 100 * time.Millisecond,
WriteDeadline: 100 * time.Millisecond,
}
// Server writes data in chunks with delays
go func() {
for range 5 {
time.Sleep(50 * time.Millisecond) // Less than the timeout
_, _ = serverConn.Write([]byte("x"))
}
}()
// Client reads multiple times - each read should reset the deadline
for i := range 5 {
buf := make([]byte, 1)
n, err := dc.Read(buf)
as.NoError(err, "read %d should succeed", i)
as.Equal(1, n)
}
}