mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48: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.
167 lines
4.4 KiB
Go
167 lines
4.4 KiB
Go
package backedpipe
|
|
|
|
import (
|
|
"io"
|
|
"sync"
|
|
)
|
|
|
|
// BackedReader wraps an unreliable io.Reader and makes it resilient to disconnections.
|
|
// It tracks sequence numbers for all bytes read and can handle reconnection,
|
|
// blocking reads when disconnected instead of erroring.
|
|
type BackedReader struct {
|
|
mu sync.Mutex
|
|
cond *sync.Cond
|
|
reader io.Reader
|
|
sequenceNum uint64
|
|
closed bool
|
|
|
|
// Error channel for generation-aware error reporting
|
|
errorEventChan chan<- ErrorEvent
|
|
|
|
// Current connection generation for error reporting
|
|
currentGen uint64
|
|
}
|
|
|
|
// NewBackedReader creates a new BackedReader with generation-aware error reporting.
|
|
// The reader is initially disconnected and must be connected using Reconnect before
|
|
// reads will succeed. The errorEventChan will receive ErrorEvent structs containing
|
|
// error details, component info, and connection generation.
|
|
func NewBackedReader(errorEventChan chan<- ErrorEvent) *BackedReader {
|
|
if errorEventChan == nil {
|
|
panic("error event channel cannot be nil")
|
|
}
|
|
br := &BackedReader{
|
|
errorEventChan: errorEventChan,
|
|
}
|
|
br.cond = sync.NewCond(&br.mu)
|
|
return br
|
|
}
|
|
|
|
// Read implements io.Reader. It blocks when disconnected until either:
|
|
// 1. A reconnection is established
|
|
// 2. The reader is closed
|
|
//
|
|
// When connected, it reads from the underlying reader and updates sequence numbers.
|
|
// Connection failures are automatically detected and reported to the higher layer via callback.
|
|
func (br *BackedReader) Read(p []byte) (int, error) {
|
|
br.mu.Lock()
|
|
defer br.mu.Unlock()
|
|
|
|
for {
|
|
// Step 1: Wait until we have a reader or are closed
|
|
for br.reader == nil && !br.closed {
|
|
br.cond.Wait()
|
|
}
|
|
|
|
if br.closed {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
// Step 2: Perform the read while holding the mutex
|
|
// This ensures proper synchronization with Reconnect and Close operations
|
|
n, err := br.reader.Read(p)
|
|
br.sequenceNum += uint64(n) // #nosec G115 -- n is always >= 0 per io.Reader contract
|
|
|
|
if err == nil {
|
|
return n, nil
|
|
}
|
|
|
|
// Mark reader as disconnected so future reads will wait for reconnection
|
|
br.reader = nil
|
|
|
|
// Notify parent of error with generation information
|
|
select {
|
|
case br.errorEventChan <- ErrorEvent{
|
|
Err: err,
|
|
Component: "reader",
|
|
Generation: br.currentGen,
|
|
}:
|
|
default:
|
|
// Channel is full, drop the error.
|
|
// This is not a problem, because we set the reader to nil
|
|
// and block until reconnected so no new errors will be sent
|
|
// until pipe processes the error and reconnects.
|
|
}
|
|
|
|
// If we got some data before the error, return it now
|
|
if n > 0 {
|
|
return n, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reconnect coordinates reconnection using channels for better synchronization.
|
|
// The seqNum channel is used to send the current sequence number to the caller.
|
|
// The newR channel is used to receive the new reader from the caller.
|
|
// This allows for better coordination during the reconnection process.
|
|
func (br *BackedReader) Reconnect(seqNum chan<- uint64, newR <-chan io.Reader) {
|
|
// Grab the lock
|
|
br.mu.Lock()
|
|
defer br.mu.Unlock()
|
|
|
|
if br.closed {
|
|
// Close the channel to indicate closed state
|
|
close(seqNum)
|
|
return
|
|
}
|
|
|
|
// Get the sequence number to send to the other side via seqNum channel
|
|
seqNum <- br.sequenceNum
|
|
close(seqNum)
|
|
|
|
// Wait for the reconnect to complete, via newR channel, and give us a new io.Reader
|
|
newReader := <-newR
|
|
|
|
// If reconnection fails while we are starting it, the caller sends nil on newR
|
|
if newReader == nil {
|
|
// Reconnection failed, keep current state
|
|
return
|
|
}
|
|
|
|
// Reconnection successful
|
|
br.reader = newReader
|
|
|
|
// Notify any waiting reads via the cond
|
|
br.cond.Broadcast()
|
|
}
|
|
|
|
// Close the reader and wake up any blocked reads.
|
|
// After closing, all Read calls will return io.EOF.
|
|
func (br *BackedReader) Close() error {
|
|
br.mu.Lock()
|
|
defer br.mu.Unlock()
|
|
|
|
if br.closed {
|
|
return nil
|
|
}
|
|
|
|
br.closed = true
|
|
br.reader = nil
|
|
|
|
// Wake up any blocked reads
|
|
br.cond.Broadcast()
|
|
|
|
return nil
|
|
}
|
|
|
|
// SequenceNum returns the current sequence number (total bytes read).
|
|
func (br *BackedReader) SequenceNum() uint64 {
|
|
br.mu.Lock()
|
|
defer br.mu.Unlock()
|
|
return br.sequenceNum
|
|
}
|
|
|
|
// Connected returns whether the reader is currently connected.
|
|
func (br *BackedReader) Connected() bool {
|
|
br.mu.Lock()
|
|
defer br.mu.Unlock()
|
|
return br.reader != nil
|
|
}
|
|
|
|
// SetGeneration sets the current connection generation for error reporting.
|
|
func (br *BackedReader) SetGeneration(generation uint64) {
|
|
br.mu.Lock()
|
|
defer br.mu.Unlock()
|
|
br.currentGen = generation
|
|
}
|