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
153 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|