mirror of
https://github.com/coder/coder.git
synced 2026-06-04 05:28:20 +00:00
4c98decfb7
Relates to: https://github.com/coder/coder/issues/18101 This PR introduces a new `backedpipe` package that provides reliable bidirectional byte streams over unreliable network connections. The implementation includes: - `BackedPipe`: Orchestrates a reader and writer to provide transparent reconnection and data replay - `BackedReader`: Handles reading with automatic reconnection, blocking reads when disconnected - `BackedWriter`: Maintains a ring buffer of recent writes for replay during reconnection - `RingBuffer`: Efficient circular buffer implementation for storing data The package enables resilient connections by tracking sequence numbers and replaying missed data after reconnection. It handles connection failures gracefully, automatically reconnecting and resuming data transfer from the appropriate point.
262 lines
5.6 KiB
Go
262 lines
5.6 KiB
Go
package backedpipe
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/goleak"
|
|
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
if runtime.GOOS == "windows" {
|
|
// Don't run goleak on windows tests, they're super flaky right now.
|
|
// See: https://github.com/coder/coder/issues/8954
|
|
os.Exit(m.Run())
|
|
}
|
|
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
|
|
}
|
|
|
|
func TestRingBuffer_NewRingBuffer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(100)
|
|
// Test that we can write and read from the buffer
|
|
rb.Write([]byte("test"))
|
|
|
|
data, err := rb.ReadLast(4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("test"), data)
|
|
}
|
|
|
|
func TestRingBuffer_WriteAndRead(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(10)
|
|
|
|
// Write some data
|
|
rb.Write([]byte("hello"))
|
|
|
|
// Read last 4 bytes
|
|
data, err := rb.ReadLast(4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ello", string(data))
|
|
|
|
// Write more data
|
|
rb.Write([]byte("world"))
|
|
|
|
// Read last 5 bytes
|
|
data, err = rb.ReadLast(5)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "world", string(data))
|
|
|
|
// Read last 3 bytes
|
|
data, err = rb.ReadLast(3)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "rld", string(data))
|
|
|
|
// Read more than available (should be 10 bytes total)
|
|
_, err = rb.ReadLast(15)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "requested 15 bytes but only")
|
|
}
|
|
|
|
func TestRingBuffer_OverflowEviction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(5)
|
|
|
|
// Fill buffer
|
|
rb.Write([]byte("abcde"))
|
|
|
|
// Overflow should evict oldest data
|
|
rb.Write([]byte("fg"))
|
|
|
|
// Should now contain "cdefg"
|
|
data, err := rb.ReadLast(5)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("cdefg"), data)
|
|
}
|
|
|
|
func TestRingBuffer_LargeWrite(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(5)
|
|
|
|
// Write data larger than capacity
|
|
rb.Write([]byte("abcdefghij"))
|
|
|
|
// Should contain last 5 bytes
|
|
data, err := rb.ReadLast(5)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("fghij"), data)
|
|
}
|
|
|
|
func TestRingBuffer_WrapAround(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(5)
|
|
|
|
// Fill buffer
|
|
rb.Write([]byte("abcde"))
|
|
|
|
// Write more to cause wrap-around
|
|
rb.Write([]byte("fgh"))
|
|
|
|
// Should contain "defgh"
|
|
data, err := rb.ReadLast(5)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("defgh"), data)
|
|
|
|
// Test reading last 3 bytes after wrap
|
|
data, err = rb.ReadLast(3)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("fgh"), data)
|
|
}
|
|
|
|
func TestRingBuffer_ReadLastEdgeCases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(3)
|
|
|
|
// Write some data (5 bytes to a 3-byte buffer, so only last 3 bytes remain)
|
|
rb.Write([]byte("hello"))
|
|
|
|
// Test reading negative count
|
|
data, err := rb.ReadLast(-1)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "cannot read negative number of bytes")
|
|
require.Nil(t, data)
|
|
|
|
// Test reading zero bytes
|
|
data, err = rb.ReadLast(0)
|
|
require.NoError(t, err)
|
|
require.Nil(t, data)
|
|
|
|
// Test reading more than available (buffer has 3 bytes, try to read 10)
|
|
_, err = rb.ReadLast(10)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "requested 10 bytes but only 3 available")
|
|
|
|
// Test reading exact amount available
|
|
data, err = rb.ReadLast(3)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("llo"), data)
|
|
}
|
|
|
|
func TestRingBuffer_EmptyWrite(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(10)
|
|
|
|
// Write empty data
|
|
rb.Write([]byte{})
|
|
|
|
// Buffer should still be empty
|
|
_, err := rb.ReadLast(5)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "requested 5 bytes but only 0 available")
|
|
}
|
|
|
|
func TestRingBuffer_MultipleWrites(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(10)
|
|
|
|
// Write data in chunks
|
|
rb.Write([]byte("ab"))
|
|
rb.Write([]byte("cd"))
|
|
rb.Write([]byte("ef"))
|
|
|
|
data, err := rb.ReadLast(6)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("abcdef"), data)
|
|
|
|
// Test partial reads
|
|
data, err = rb.ReadLast(4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("cdef"), data)
|
|
|
|
data, err = rb.ReadLast(2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("ef"), data)
|
|
}
|
|
|
|
func TestRingBuffer_EdgeCaseEviction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(3)
|
|
|
|
// Write data that will cause eviction
|
|
rb.Write([]byte("abc"))
|
|
|
|
// Write more to cause eviction
|
|
rb.Write([]byte("d"))
|
|
|
|
// Should now contain "bcd"
|
|
data, err := rb.ReadLast(3)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("bcd"), data)
|
|
}
|
|
|
|
func TestRingBuffer_ComplexWrapAroundScenario(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rb := newRingBuffer(8)
|
|
|
|
// Fill buffer
|
|
rb.Write([]byte("12345678"))
|
|
|
|
// Evict some and add more to create complex wrap scenario
|
|
rb.Write([]byte("abcd"))
|
|
data, err := rb.ReadLast(8)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("5678abcd"), data)
|
|
|
|
// Add more
|
|
rb.Write([]byte("xyz"))
|
|
data, err = rb.ReadLast(8)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("8abcdxyz"), data)
|
|
|
|
// Test reading various amounts from the end
|
|
data, err = rb.ReadLast(7)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("abcdxyz"), data)
|
|
|
|
data, err = rb.ReadLast(4)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("dxyz"), data)
|
|
}
|
|
|
|
// Benchmark tests for performance validation
|
|
func BenchmarkRingBuffer_Write(b *testing.B) {
|
|
rb := newRingBuffer(64 * 1024 * 1024) // 64MB for benchmarks
|
|
data := bytes.Repeat([]byte("x"), 1024) // 1KB writes
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
rb.Write(data)
|
|
}
|
|
}
|
|
|
|
func BenchmarkRingBuffer_ReadLast(b *testing.B) {
|
|
rb := newRingBuffer(64 * 1024 * 1024) // 64MB for benchmarks
|
|
// Fill buffer with test data
|
|
for i := 0; i < 64; i++ {
|
|
rb.Write(bytes.Repeat([]byte("x"), 1024))
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := rb.ReadLast((i % 100) + 1)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|