mirror of
https://github.com/soypat/lneto.git
synced 2026-08-21 23:19:03 +00:00
Add out-of-order segment reassembly (#148)
* feat(tcp): add out-of-order segment reassembly Add an opt-in, bounded out-of-order reassembly buffer so a single lost segment can be recovered by retransmitting the gap while later segments are held and delivered once the gap fills. The receiver also subtracts buffered out-of-order bytes from the advertised receive window and avoids challenge-ACK aborts for in-window future data. Reassembly is disabled by default. Generated with LLM assistance. Signed-off-by: Marvin Drees <marvin.drees@9elements.com> * implement review feedback around rx buffer reuse Signed-off-by: Marvin Drees <marvin.drees@9elements.com> --------- Signed-off-by: Marvin Drees <marvin.drees@9elements.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
var (
|
||||
ErrRingBufferFull = lneto.ErrBufferFull
|
||||
errRingNoData = errors.New("lneto/ring: empty write")
|
||||
errInvalidCommit = errors.New("lneto/ring: invalid commit amount")
|
||||
errInvalidDiscard = errors.New("lneto/ring: invalid discard amount")
|
||||
errDiscardExceeds = errors.New("lneto/ring: discard exceeds length")
|
||||
errOffsetOverflow = errors.New("lneto/ring: offset too large (32 bit overflow)")
|
||||
@@ -92,6 +93,58 @@ func (r *Ring) Write(b []byte) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// writeStart returns the buffer index where the next [Ring.Write] or
|
||||
// [Ring.Commit] begins, matching [Ring.Write]'s placement (including wrap).
|
||||
func (r *Ring) writeStart() int {
|
||||
if r.End == 0 {
|
||||
return r.Off // Empty: writing begins at Off.
|
||||
}
|
||||
if r.End == len(r.Buf) {
|
||||
return 0 // Tail full: next byte wraps to the start.
|
||||
}
|
||||
return r.End
|
||||
}
|
||||
|
||||
// PeekWrite stages b offset bytes past the write position (see
|
||||
// [Ring.writeStart]) without advancing it, so the bytes are not yet readable; a
|
||||
// later [Ring.Commit] reveals them. It reports false, writing nothing, when
|
||||
// offset is negative or offset+len(b) exceeds [Ring.Free]. Used to place
|
||||
// out-of-order data ahead of a gap that a normal Write later fills.
|
||||
func (r *Ring) PeekWrite(b []byte, offset int) bool {
|
||||
if offset < 0 || offset+len(b) > r.Free() {
|
||||
return false
|
||||
}
|
||||
off := r.writeStart() + offset
|
||||
if off >= len(r.Buf) {
|
||||
off -= len(r.Buf)
|
||||
}
|
||||
n := copy(r.Buf[off:], b)
|
||||
if n < len(b) {
|
||||
copy(r.Buf, b[n:])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Commit advances the write pointer by n bytes, making readable any bytes
|
||||
// previously staged with [Ring.PeekWrite]. It copies nothing and errors if n is
|
||||
// not positive or exceeds [Ring.Free].
|
||||
func (r *Ring) Commit(n int) error {
|
||||
if n <= 0 {
|
||||
return errInvalidCommit
|
||||
} else if n > r.Free() {
|
||||
return ErrRingBufferFull
|
||||
}
|
||||
if r.End == 0 {
|
||||
r.End = r.Off // Match Write: commit begins at Off when empty.
|
||||
}
|
||||
end := r.End + n
|
||||
if end > len(r.Buf) {
|
||||
end -= len(r.Buf)
|
||||
}
|
||||
r.End = end // Never 0 here: end==len(Buf) is kept.
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDiscard is a performance auxiliary method that performs a dummy read or no-op read
|
||||
// for advancing the read pointer n bytes without actually copying data.
|
||||
// This method panics if amount of bytes is more than buffered (see [Ring.Buffered]).
|
||||
|
||||
@@ -580,3 +580,84 @@ func canonRing(r *Ring) {
|
||||
r.onReadEnd(1)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRingPeekWriteCommit verifies that bytes staged ahead of a gap with
|
||||
// PeekWrite become readable in order once the gap is filled and committed.
|
||||
func TestRingPeekWriteCommit(t *testing.T) {
|
||||
r := &Ring{Buf: make([]byte, 16)}
|
||||
// Stage "BBBB" 4 bytes ahead of the write position (the gap).
|
||||
if !r.PeekWrite([]byte("BBBB"), 4) {
|
||||
t.Fatal("PeekWrite should fit")
|
||||
}
|
||||
// Staged bytes are not yet readable.
|
||||
if r.Buffered() != 0 {
|
||||
t.Fatalf("staged bytes must not be readable, buffered=%d", r.Buffered())
|
||||
}
|
||||
// Fill the gap with a normal write, then commit the staged tail.
|
||||
if _, err := r.Write([]byte("AAAA")); err != nil {
|
||||
t.Fatal("gap write:", err)
|
||||
}
|
||||
if err := r.Commit(4); err != nil {
|
||||
t.Fatal("commit:", err)
|
||||
}
|
||||
got := make([]byte, 8)
|
||||
n, err := r.Read(got)
|
||||
if err != nil {
|
||||
t.Fatal("read:", err)
|
||||
}
|
||||
if string(got[:n]) != "AAAABBBB" {
|
||||
t.Fatalf("read %q, want AAAABBBB", got[:n])
|
||||
}
|
||||
testRingSanity(t, r)
|
||||
}
|
||||
|
||||
// TestRingPeekWriteWrap exercises PeekWrite/Commit when the staged region wraps
|
||||
// across the end of the backing buffer. Existing data at Off=2,End=6 puts the
|
||||
// write position at index 6, so a 2-byte gap fills indices 6,7 and the staged
|
||||
// tail wraps to indices 0,1.
|
||||
func TestRingPeekWriteWrap(t *testing.T) {
|
||||
r := &Ring{Buf: make([]byte, 8)}
|
||||
setRingData(t, r, 2, []byte("WXYZ")) // Off=2, End=6, 4 bytes buffered.
|
||||
if r.writeStart() != 6 {
|
||||
t.Fatalf("writeStart=%d, want 6", r.writeStart())
|
||||
}
|
||||
// Stage "CD" 2 bytes ahead of the write position (6) → wraps to indices 0,1.
|
||||
if !r.PeekWrite([]byte("CD"), 2) {
|
||||
t.Fatal("PeekWrite (wrap) should fit")
|
||||
}
|
||||
// Fill the 2-byte gap at indices 6,7, then commit the wrapped tail.
|
||||
if _, err := r.Write([]byte("AB")); err != nil {
|
||||
t.Fatal("gap write:", err)
|
||||
}
|
||||
if err := r.Commit(2); err != nil {
|
||||
t.Fatal("commit:", err)
|
||||
}
|
||||
got := make([]byte, 8)
|
||||
n, err := r.Read(got)
|
||||
if err != nil {
|
||||
t.Fatal("read:", err)
|
||||
}
|
||||
if string(got[:n]) != "WXYZABCD" {
|
||||
t.Fatalf("read %q, want WXYZABCD", got[:n])
|
||||
}
|
||||
testRingSanity(t, r)
|
||||
}
|
||||
|
||||
func TestRingPeekWriteRejects(t *testing.T) {
|
||||
r := &Ring{Buf: make([]byte, 8)}
|
||||
if r.PeekWrite([]byte("toolong!!"), 0) {
|
||||
t.Error("PeekWrite must reject data larger than the buffer")
|
||||
}
|
||||
if r.PeekWrite([]byte("data"), 5) { // 5+4 > 8 free.
|
||||
t.Error("PeekWrite must reject offset+len beyond free space")
|
||||
}
|
||||
if r.PeekWrite([]byte("x"), -1) {
|
||||
t.Error("PeekWrite must reject negative offset")
|
||||
}
|
||||
if err := r.Commit(0); err == nil {
|
||||
t.Error("Commit(0) must error")
|
||||
}
|
||||
if err := r.Commit(9); err == nil {
|
||||
t.Error("Commit beyond free must error")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user