detached 43a6679

improve(dgram): pace UDP data with congestion feedback

commit
43a66791ca8e55b1ec503641b35f9544e115da1c
author
brice · · 2026-07-10 07:11 UTC
parent
7f56b4a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +609 -369
A internal/adapters/dgram/congestion.go +68 -0
@@ -0,0 +1,68 @@
1+package dgram
2+
3+import "time"
4+
5+const (
6+ initialCongestionPackets = 10
7+ minimumCongestionPackets = 2
8+ maximumCongestionBytes = 4 << 20
9+ initialPacingRTT = 250 * time.Millisecond
10+ dataBurstPackets = 2
11+)
12+
13+type congestionController struct {
14+ mtu int
15+ cwndBytes int
16+ minBytes int
17+ maxBytes int
18+ srtt time.Duration
19+}
20+
21+func newCongestionController(mtu int) congestionController {
22+ minimum := minimumCongestionPackets * mtu
23+ maximum := maximumCongestionBytes
24+ if maximum < minimum {
25+ maximum = minimum
26+ }
27+ initial := initialCongestionPackets * mtu
28+ if initial > maximum {
29+ initial = maximum
30+ }
31+ return congestionController{
32+ mtu: mtu,
33+ cwndBytes: initial,
34+ minBytes: minimum,
35+ maxBytes: maximum,
36+ srtt: initialPacingRTT,
37+ }
38+}
39+
40+func (c *congestionController) onRTT(srtt time.Duration) {
41+ if srtt > 0 {
42+ c.srtt = srtt
43+ }
44+}
45+
46+func (c *congestionController) onACK(ackedBytes int) {
47+ if ackedBytes <= 0 || c.cwndBytes >= c.maxBytes {
48+ return
49+ }
50+ increase := int(int64(c.mtu) * int64(ackedBytes) / int64(c.cwndBytes))
51+ if increase < 1 {
52+ increase = 1
53+ }
54+ c.cwndBytes = min(c.cwndBytes+increase, c.maxBytes)
55+}
56+
57+func (c *congestionController) onLoss() {
58+ c.cwndBytes = max(c.cwndBytes/2, c.minBytes)
59+}
60+
61+func (c congestionController) bytesPerSecond() int {
62+ rtt := max(c.srtt, time.Millisecond)
63+ return int(int64(c.cwndBytes) * int64(time.Second) / int64(rtt))
64+}
65+
66+func (c congestionController) burstBytes() int {
67+ return min(c.cwndBytes, dataBurstPackets*c.mtu)
68+}
@@ -0,0 +1,68 @@
1+package dgram
2+
3+import "time"
4+
5+const (
6+ initialCongestionPackets = 10
7+ minimumCongestionPackets = 2
8+ maximumCongestionBytes = 4 << 20
9+ initialPacingRTT = 250 * time.Millisecond
10+ dataBurstPackets = 2
11+)
12+
13+type congestionController struct {
14+ mtu int
15+ cwndBytes int
16+ minBytes int
17+ maxBytes int
18+ srtt time.Duration
19+}
20+
21+func newCongestionController(mtu int) congestionController {
22+ minimum := minimumCongestionPackets * mtu
23+ maximum := maximumCongestionBytes
24+ if maximum < minimum {
25+ maximum = minimum
26+ }
27+ initial := initialCongestionPackets * mtu
28+ if initial > maximum {
29+ initial = maximum
30+ }
31+ return congestionController{
32+ mtu: mtu,
33+ cwndBytes: initial,
34+ minBytes: minimum,
35+ maxBytes: maximum,
36+ srtt: initialPacingRTT,
37+ }
38+}
39+
40+func (c *congestionController) onRTT(srtt time.Duration) {
41+ if srtt > 0 {
42+ c.srtt = srtt
43+ }
44+}
45+
46+func (c *congestionController) onACK(ackedBytes int) {
47+ if ackedBytes <= 0 || c.cwndBytes >= c.maxBytes {
48+ return
49+ }
50+ increase := int(int64(c.mtu) * int64(ackedBytes) / int64(c.cwndBytes))
51+ if increase < 1 {
52+ increase = 1
53+ }
54+ c.cwndBytes = min(c.cwndBytes+increase, c.maxBytes)
55+}
56+
57+func (c *congestionController) onLoss() {
58+ c.cwndBytes = max(c.cwndBytes/2, c.minBytes)
59+}
60+
61+func (c congestionController) bytesPerSecond() int {
62+ rtt := max(c.srtt, time.Millisecond)
63+ return int(int64(c.cwndBytes) * int64(time.Second) / int64(rtt))
64+}
65+
66+func (c congestionController) burstBytes() int {
67+ return min(c.cwndBytes, dataBurstPackets*c.mtu)
68+}
M internal/adapters/dgram/network_sim_test.go +140 -49
@@ -54,6 +54,7 @@ type simulatedLink struct {
5454 queue []scheduledPacket
5555 queueBytes int
5656 next time.Time
57+ dropped int
5758 }
5859
5960 func newSimulatedLink(clock interface{ Now() time.Time }, policy packetPolicy) *simulatedLink {
@@ -71,6 +72,7 @@ func (l *simulatedLink) enqueue(p *fakePC, peer *fakePC, b []byte) bool {
7172 return l.deliverLocked(peer, packet{b: append([]byte(nil), b...), addr: p.addr})
7273 }
7374 if l.policy.MaxQueueBytes <= 0 || l.queueBytes+len(b) > l.policy.MaxQueueBytes {
75+ l.dropped++
7476 return false
7577 }
7678 due := meta.At.Add(l.policy.Delay)
@@ -107,6 +109,12 @@ func (l *simulatedLink) queuedBytes() int {
107109 return l.queueBytes
108110 }
109111
112+func (l *simulatedLink) droppedPackets() int {
113+ l.mu.Lock()
114+ defer l.mu.Unlock()
115+ return l.dropped
116+}
117+
110118 func (l *simulatedLink) deliverLocked(peer *fakePC, q packet) bool {
111119 peer.mu.Lock()
112120 defer peer.mu.Unlock()
@@ -281,8 +289,8 @@ func TestTransportFloodClassification(t *testing.T) {
281289 if len(dropped) != floodRecordCount {
282290 t.Fatalf("flood state = %+v, dropped records=%d, want exactly one fragment from each of %d records", state, len(dropped), floodRecordCount)
283291 }
284- if state.packetAge != 0 || state.recordAge != time.Nanosecond || state.ackAge != time.Nanosecond {
285- t.Fatalf("flood state = %+v, want packet contact only after %d output records of %d MTUs", state, floodRecordCount, floodRecordMTUs)
292+ if state.packetAge > initialPacingRTT || state.recordAge <= 0 || state.ackAge <= 0 {
293+ t.Fatalf("flood state = %+v, want fresh packet contact without record or ACK progress", state)
286294 }
287295 if !clk.Now().After(start) {
288296 t.Fatal("real authenticated fragments did not update contact")
@@ -291,34 +299,90 @@ func TestTransportFloodClassification(t *testing.T) {
291299
292300 t.Run("bounded byte queue advances and releases on fake clock", func(t *testing.T) {
293301 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
294- limit := 2 * 128
302+ const mtu = 128
303+ limit := dataBurstPackets * mtu
295304 link := newSimulatedLink(clk, packetPolicy{Delay: time.Second, MaxQueueBytes: limit, BytesPerSecond: 128})
296305 aPC, bPC := newFloodPair(link)
297- const mtu = 128
298- a, b := newFloodTransports(t, aPC, bPC, clk, mtu)
306+ bPC.link = nil // ACKs are unconstrained; this case isolates outbound congestion.
307+ opts := Options{
308+ Clock: clk, MTU: mtu, ResendAfter: time.Hour, Heartbeat: time.Hour,
309+ DegradedAfter: time.Hour, ProbeAfter: time.Hour, OfflineAfter: time.Hour, DeadAfter: time.Hour,
310+ }
311+ a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, opts)
312+ if err != nil {
313+ t.Fatal(err)
314+ }
315+ b, err := NewTransportWithOptions(bPC, aPC.addr, key(), 2, 1, opts)
316+ if err != nil {
317+ _ = a.Close()
318+ t.Fatal(err)
319+ }
299320 defer closeFloodTransports(a, b)
300321 contacted := make(chan struct{}, 1)
301322 b.mu.Lock()
302- b.afterAuthenticatedPacket = func() { contacted <- struct{}{} }
323+ b.afterAuthenticatedPacket = func() {
324+ select {
325+ case contacted <- struct{}{}:
326+ default:
327+ }
328+ }
303329 b.mu.Unlock()
304- a.mu.Lock()
305- a.rto = time.Hour // Keep the fake-clock queue case about bandwidth, not retries.
306- a.mu.Unlock()
307- sendFloodOutputs(t, a, floodRecordCount, mtu)
330+ maxFragmentData := mtu - pdgram.HeaderSize - a.codec.Overhead()
331+ if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 4*maxFragmentData)}); err != nil {
332+ t.Fatal(err)
333+ }
334+ eventually(t, time.Second, func() bool { return link.queuedBytes() == limit })
335+ clk.advance(initialPacingRTT)
336+ time.Sleep(time.Millisecond)
308337 state := floodState(a, b, link)
309- if state.queuedBytes <= 0 || state.queuedBytes > limit {
310- t.Fatalf("flood state = %+v, want queued bytes in 1..%d", state, limit)
338+ if state.queuedBytes != limit || link.droppedPackets() == 0 {
339+ t.Fatalf("flood state = %+v drops=%d, want bounded queue overflow at %d bytes", state, link.droppedPackets(), limit)
311340 }
312- clk.advance(10 * time.Second)
341+ clk.advance(time.Second)
313342 link.flush(bPC)
314343 awaitSignal(t, contacted, "queued authenticated fragment")
315- state = floodState(a, b, link)
316- if state.queuedBytes != 0 {
317- t.Fatalf("flood state = %+v, want empty queue after fake-clock release", state)
344+ for link.queuedBytes() > 0 {
345+ clk.advance(time.Second)
346+ link.flush(bPC)
318347 }
319- if state.packetAge != 0 || state.recordAge != 10*time.Second || state.ackAge != 10*time.Second {
320- t.Fatalf("flood state = %+v, want delayed packet contact without record or ACK progress", state)
348+ if age := floodState(a, b, link).packetAge; age >= a.probeAfter {
349+ t.Fatalf("authenticated contact age=%v, want below probe threshold %v", age, a.probeAfter)
350+ }
351+ eventually(t, time.Second, func() bool {
352+ a.mu.Lock()
353+ defer a.mu.Unlock()
354+ p := a.pending[1]
355+ return p != nil && !p.initialInFlight && !p.last.IsZero()
356+ })
357+
358+ a.mu.Lock()
359+ beforeLoss := a.congestion.cwndBytes
360+ a.rto = time.Millisecond
361+ a.mu.Unlock()
362+ aPC.link = nil // The transient bottleneck clears before the paced retry.
363+ clk.advance(2 * time.Millisecond)
364+ a.queueRetransmits()
365+ eventually(t, time.Second, func() bool {
366+ a.mu.Lock()
367+ defer a.mu.Unlock()
368+ return a.congestion.cwndBytes < beforeLoss
369+ })
370+ for range 16 {
371+ clk.advance(time.Second)
372+ link.flush(bPC)
373+ clk.advance(maxACKDelay)
374+ time.Sleep(time.Millisecond)
375+ a.mu.Lock()
376+ drained := len(a.pending) == 0
377+ a.mu.Unlock()
378+ if drained {
379+ return
380+ }
321381 }
382+ a.mu.Lock()
383+ pending := a.pending[1]
384+ a.mu.Unlock()
385+ t.Fatalf("pending records did not drain after paced retransmission: %+v pending=%+v drops=%d", floodState(a, b, link), pending, link.droppedPackets())
322386 })
323387
324388 t.Run("control stays responsive while fresh output and large retransmits contend", func(t *testing.T) {
@@ -350,8 +414,6 @@ func TestTransportFloodClassification(t *testing.T) {
350414 releaseRetransmit := make(chan struct{})
351415 lastRetransmitted := make(chan struct{}, 1)
352416 probeSent := make(chan struct{}, 1)
353- freshAtPace := make(chan struct{})
354- allowFreshPace := make(chan struct{})
355417 freshReturned := make(chan error, 1)
356418 writes := make(map[uint64]int)
357419 var writesMu sync.Mutex
@@ -406,6 +468,9 @@ func TestTransportFloodClassification(t *testing.T) {
406468 for range 6 { // Both transports install retransmit, health, and heartbeat timers.
407469 awaitSignal(t, clk.timerCreated, "transport timer creation")
408470 }
471+ a.mu.Lock()
472+ a.rto = time.Hour
473+ a.mu.Unlock()
409474
410475 // Fresh, fragmented output records become overdue while their real
411476 // cumulative ACKs are lost. Disabling pacing intentionally forces overload
@@ -414,48 +479,60 @@ func TestTransportFloodClassification(t *testing.T) {
414479 awaitSignal(t, clk.timerCreated, "ACK coalescing timer creation")
415480 clk.advance(maxACKDelay)
416481 awaitSignal(t, acksDropped, "dropped cumulative ACK")
482+ a.mu.Lock()
483+ a.rto = 10 * time.Millisecond
484+ a.mu.Unlock()
485+ a.queueRetransmits()
486+ a.mu.Lock()
487+ a.rto = time.Hour
488+ a.mu.Unlock()
489+ clk.advance(initialPacingRTT)
417490
418491 awaitSignal(t, retransmitBlocked, "retransmit pacing barrier")
419492 if state := floodState(a, b, nil); state.retransmits < floodRecordCount {
420493 t.Fatalf("flood state = %+v, want all %d overdue records selected", state, floodRecordCount)
421494 }
422-
423- // Start a third large output at this same fake-clock instant. The pacing
424- // barrier proves its real Send reaches the shared pacing gate while the
425- // retransmit owns it inside WriteTo.
426- a.mu.Lock()
427- a.outputNext = time.Time{}
428- a.beforeDataPace = func() {
429- close(freshAtPace)
430- awaitSignal(t, allowFreshPace, "fresh pacing release")
495+ select {
496+ case <-probeSent:
497+ default:
431498 }
432- a.mu.Unlock()
499+
500+ // A fresh record queues behind the sole data sender while control traffic
501+ // remains independent of congestion tokens and the blocked data write.
433502 go func() {
434503 freshReturned <- a.Send(ports.Frame{Type: ports.MsgOutput, Payload: floodOutputPayload(floodRecordCount, 128)})
435504 }()
436- awaitSignal(t, freshAtPace, "fresh output pacing barrier")
437- a.mu.Lock()
438- a.beforeDataPace = nil
439- a.mu.Unlock()
440- close(allowFreshPace)
441505 select {
442506 case err := <-freshReturned:
443507 t.Fatalf("fresh output completed while retransmit held pacing: %v", err)
444508 default:
445509 }
446510
511+ clk.advance(11 * time.Millisecond)
447512 awaitSignal(t, probeSent, "heartbeat probe during output contention")
448513
449514 close(releaseRetransmit)
450- if err := awaitResult(t, freshReturned, "fresh output completion"); err != nil {
451- t.Fatal(err)
515+ for range 1024 {
516+ clk.advance(initialPacingRTT)
517+ time.Sleep(time.Millisecond)
518+ select {
519+ case err := <-freshReturned:
520+ if err != nil {
521+ t.Fatal(err)
522+ }
523+ awaitSignal(t, lastRetransmitted, "last overdue record retransmission")
524+ writesMu.Lock()
525+ defer writesMu.Unlock()
526+ if writes[1] < 2 || writes[floodRecordCount] < 2 || writes[floodRecordCount+1] == 0 {
527+ t.Fatalf("flood state = %+v, writes=%v, want fresh large traffic and retransmissions across %d overdue records", floodState(a, b, nil), writes, floodRecordCount)
528+ }
529+ return
530+ default:
531+ }
452532 }
453- awaitSignal(t, lastRetransmitted, "last overdue record retransmission")
454533 writesMu.Lock()
455534 defer writesMu.Unlock()
456- if writes[1] < 2 || writes[floodRecordCount] < 2 || writes[floodRecordCount+1] == 0 {
457- t.Fatalf("flood state = %+v, writes=%v, want fresh large traffic and retransmissions across %d overdue records", floodState(a, b, nil), writes, floodRecordCount)
458- }
535+ t.Fatalf("fresh output did not complete after paced retransmissions: writes=%v state=%+v", writes, floodState(a, b, nil))
459536 })
460537 }
461538
@@ -510,23 +587,37 @@ func TestFloodOutputPayloadIsIncrementalStateBearingOutput(t *testing.T) {
510587
511588 func sendFloodOutputs(t *testing.T, sender *Transport, count, mtu int) {
512589 t.Helper()
513- sender.dataPaceMu.Lock()
514- sender.dataPaceRemaining = count * floodRecordMTUs * 4
515- sender.dataPaceNext = sender.clock.Now().Add(time.Hour)
516- sender.dataPaceMu.Unlock()
517590 for state := range count {
591+ if err := sender.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: floodOutputPayload(state, mtu)}); err != nil {
592+ t.Fatal(err)
593+ }
594+ }
595+ clk, ok := sender.clock.(*manualClock)
596+ if !ok {
597+ return
598+ }
599+ for range count * floodRecordMTUs * 2 {
600+ clk.advance(initialPacingRTT)
601+ time.Sleep(time.Millisecond)
518602 sender.mu.Lock()
519- sender.outputNext = time.Time{}
603+ complete := len(sender.outputQueue) == 0 && len(sender.dataSend) == 0
604+ for _, p := range sender.pending {
605+ complete = complete && !p.initialInFlight && !p.last.IsZero()
606+ }
520607 sender.mu.Unlock()
521- if err := sender.Send(ports.Frame{Type: ports.MsgOutput, Payload: floodOutputPayload(state, mtu)}); err != nil {
522- t.Fatal(err)
608+ if complete {
609+ return
523610 }
524611 }
612+ t.Fatal("flood output pacing did not drain")
525613 }
526614
527615 func newFloodTransports(t *testing.T, aPC, bPC *fakePC, clk *manualClock, mtu int) (*Transport, *Transport) {
528616 t.Helper()
529- opts := Options{Clock: clk, MTU: mtu, ResendAfter: 10 * time.Millisecond, Heartbeat: time.Hour}
617+ opts := Options{
618+ Clock: clk, MTU: mtu, ResendAfter: time.Hour, Heartbeat: time.Hour,
619+ DegradedAfter: time.Hour, ProbeAfter: time.Hour, OfflineAfter: time.Hour, DeadAfter: time.Hour,
620+ }
530621 a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, opts)
531622 if err != nil {
532623 t.Fatal(err)
@@ -54,6 +54,7 @@ type simulatedLink struct {
54 queue []scheduledPacket54 queue []scheduledPacket
55 queueBytes int55 queueBytes int
56 next time.Time56 next time.Time
57+ dropped int
57 }58 }
58 59
59 func newSimulatedLink(clock interface{ Now() time.Time }, policy packetPolicy) *simulatedLink {60 func newSimulatedLink(clock interface{ Now() time.Time }, policy packetPolicy) *simulatedLink {
@@ -71,6 +72,7 @@ func (l *simulatedLink) enqueue(p *fakePC, peer *fakePC, b []byte) bool {
71 return l.deliverLocked(peer, packet{b: append([]byte(nil), b...), addr: p.addr})72 return l.deliverLocked(peer, packet{b: append([]byte(nil), b...), addr: p.addr})
72 }73 }
73 if l.policy.MaxQueueBytes <= 0 || l.queueBytes+len(b) > l.policy.MaxQueueBytes {74 if l.policy.MaxQueueBytes <= 0 || l.queueBytes+len(b) > l.policy.MaxQueueBytes {
75+ l.dropped++
74 return false76 return false
75 }77 }
76 due := meta.At.Add(l.policy.Delay)78 due := meta.At.Add(l.policy.Delay)
@@ -107,6 +109,12 @@ func (l *simulatedLink) queuedBytes() int {
107 return l.queueBytes109 return l.queueBytes
108 }110 }
109 111
112+func (l *simulatedLink) droppedPackets() int {
113+ l.mu.Lock()
114+ defer l.mu.Unlock()
115+ return l.dropped
116+}
117+
110 func (l *simulatedLink) deliverLocked(peer *fakePC, q packet) bool {118 func (l *simulatedLink) deliverLocked(peer *fakePC, q packet) bool {
111 peer.mu.Lock()119 peer.mu.Lock()
112 defer peer.mu.Unlock()120 defer peer.mu.Unlock()
@@ -281,8 +289,8 @@ func TestTransportFloodClassification(t *testing.T) {
281 if len(dropped) != floodRecordCount {289 if len(dropped) != floodRecordCount {
282 t.Fatalf("flood state = %+v, dropped records=%d, want exactly one fragment from each of %d records", state, len(dropped), floodRecordCount)290 t.Fatalf("flood state = %+v, dropped records=%d, want exactly one fragment from each of %d records", state, len(dropped), floodRecordCount)
283 }291 }
284- if state.packetAge != 0 || state.recordAge != time.Nanosecond || state.ackAge != time.Nanosecond {292+ if state.packetAge > initialPacingRTT || state.recordAge <= 0 || state.ackAge <= 0 {
285- t.Fatalf("flood state = %+v, want packet contact only after %d output records of %d MTUs", state, floodRecordCount, floodRecordMTUs)293+ t.Fatalf("flood state = %+v, want fresh packet contact without record or ACK progress", state)
286 }294 }
287 if !clk.Now().After(start) {295 if !clk.Now().After(start) {
288 t.Fatal("real authenticated fragments did not update contact")296 t.Fatal("real authenticated fragments did not update contact")
@@ -291,34 +299,90 @@ func TestTransportFloodClassification(t *testing.T) {
291 299
292 t.Run("bounded byte queue advances and releases on fake clock", func(t *testing.T) {300 t.Run("bounded byte queue advances and releases on fake clock", func(t *testing.T) {
293 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))301 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
294- limit := 2 * 128302+ const mtu = 128
303+ limit := dataBurstPackets * mtu
295 link := newSimulatedLink(clk, packetPolicy{Delay: time.Second, MaxQueueBytes: limit, BytesPerSecond: 128})304 link := newSimulatedLink(clk, packetPolicy{Delay: time.Second, MaxQueueBytes: limit, BytesPerSecond: 128})
296 aPC, bPC := newFloodPair(link)305 aPC, bPC := newFloodPair(link)
297- const mtu = 128306+ bPC.link = nil // ACKs are unconstrained; this case isolates outbound congestion.
298- a, b := newFloodTransports(t, aPC, bPC, clk, mtu)307+ opts := Options{
308+ Clock: clk, MTU: mtu, ResendAfter: time.Hour, Heartbeat: time.Hour,
309+ DegradedAfter: time.Hour, ProbeAfter: time.Hour, OfflineAfter: time.Hour, DeadAfter: time.Hour,
310+ }
311+ a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, opts)
312+ if err != nil {
313+ t.Fatal(err)
314+ }
315+ b, err := NewTransportWithOptions(bPC, aPC.addr, key(), 2, 1, opts)
316+ if err != nil {
317+ _ = a.Close()
318+ t.Fatal(err)
319+ }
299 defer closeFloodTransports(a, b)320 defer closeFloodTransports(a, b)
300 contacted := make(chan struct{}, 1)321 contacted := make(chan struct{}, 1)
301 b.mu.Lock()322 b.mu.Lock()
302- b.afterAuthenticatedPacket = func() { contacted <- struct{}{} }323+ b.afterAuthenticatedPacket = func() {
324+ select {
325+ case contacted <- struct{}{}:
326+ default:
327+ }
328+ }
303 b.mu.Unlock()329 b.mu.Unlock()
304- a.mu.Lock()330+ maxFragmentData := mtu - pdgram.HeaderSize - a.codec.Overhead()
305- a.rto = time.Hour // Keep the fake-clock queue case about bandwidth, not retries.331+ if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 4*maxFragmentData)}); err != nil {
306- a.mu.Unlock()332+ t.Fatal(err)
307- sendFloodOutputs(t, a, floodRecordCount, mtu)333+ }
334+ eventually(t, time.Second, func() bool { return link.queuedBytes() == limit })
335+ clk.advance(initialPacingRTT)
336+ time.Sleep(time.Millisecond)
308 state := floodState(a, b, link)337 state := floodState(a, b, link)
309- if state.queuedBytes <= 0 || state.queuedBytes > limit {338+ if state.queuedBytes != limit || link.droppedPackets() == 0 {
310- t.Fatalf("flood state = %+v, want queued bytes in 1..%d", state, limit)339+ t.Fatalf("flood state = %+v drops=%d, want bounded queue overflow at %d bytes", state, link.droppedPackets(), limit)
311 }340 }
312- clk.advance(10 * time.Second)341+ clk.advance(time.Second)
313 link.flush(bPC)342 link.flush(bPC)
314 awaitSignal(t, contacted, "queued authenticated fragment")343 awaitSignal(t, contacted, "queued authenticated fragment")
315- state = floodState(a, b, link)344+ for link.queuedBytes() > 0 {
316- if state.queuedBytes != 0 {345+ clk.advance(time.Second)
317- t.Fatalf("flood state = %+v, want empty queue after fake-clock release", state)346+ link.flush(bPC)
318 }347 }
319- if state.packetAge != 0 || state.recordAge != 10*time.Second || state.ackAge != 10*time.Second {348+ if age := floodState(a, b, link).packetAge; age >= a.probeAfter {
320- t.Fatalf("flood state = %+v, want delayed packet contact without record or ACK progress", state)349+ t.Fatalf("authenticated contact age=%v, want below probe threshold %v", age, a.probeAfter)
350+ }
351+ eventually(t, time.Second, func() bool {
352+ a.mu.Lock()
353+ defer a.mu.Unlock()
354+ p := a.pending[1]
355+ return p != nil && !p.initialInFlight && !p.last.IsZero()
356+ })
357+
358+ a.mu.Lock()
359+ beforeLoss := a.congestion.cwndBytes
360+ a.rto = time.Millisecond
361+ a.mu.Unlock()
362+ aPC.link = nil // The transient bottleneck clears before the paced retry.
363+ clk.advance(2 * time.Millisecond)
364+ a.queueRetransmits()
365+ eventually(t, time.Second, func() bool {
366+ a.mu.Lock()
367+ defer a.mu.Unlock()
368+ return a.congestion.cwndBytes < beforeLoss
369+ })
370+ for range 16 {
371+ clk.advance(time.Second)
372+ link.flush(bPC)
373+ clk.advance(maxACKDelay)
374+ time.Sleep(time.Millisecond)
375+ a.mu.Lock()
376+ drained := len(a.pending) == 0
377+ a.mu.Unlock()
378+ if drained {
379+ return
380+ }
321 }381 }
382+ a.mu.Lock()
383+ pending := a.pending[1]
384+ a.mu.Unlock()
385+ t.Fatalf("pending records did not drain after paced retransmission: %+v pending=%+v drops=%d", floodState(a, b, link), pending, link.droppedPackets())
322 })386 })
323 387
324 t.Run("control stays responsive while fresh output and large retransmits contend", func(t *testing.T) {388 t.Run("control stays responsive while fresh output and large retransmits contend", func(t *testing.T) {
@@ -350,8 +414,6 @@ func TestTransportFloodClassification(t *testing.T) {
350 releaseRetransmit := make(chan struct{})414 releaseRetransmit := make(chan struct{})
351 lastRetransmitted := make(chan struct{}, 1)415 lastRetransmitted := make(chan struct{}, 1)
352 probeSent := make(chan struct{}, 1)416 probeSent := make(chan struct{}, 1)
353- freshAtPace := make(chan struct{})
354- allowFreshPace := make(chan struct{})
355 freshReturned := make(chan error, 1)417 freshReturned := make(chan error, 1)
356 writes := make(map[uint64]int)418 writes := make(map[uint64]int)
357 var writesMu sync.Mutex419 var writesMu sync.Mutex
@@ -406,6 +468,9 @@ func TestTransportFloodClassification(t *testing.T) {
406 for range 6 { // Both transports install retransmit, health, and heartbeat timers.468 for range 6 { // Both transports install retransmit, health, and heartbeat timers.
407 awaitSignal(t, clk.timerCreated, "transport timer creation")469 awaitSignal(t, clk.timerCreated, "transport timer creation")
408 }470 }
471+ a.mu.Lock()
472+ a.rto = time.Hour
473+ a.mu.Unlock()
409 474
410 // Fresh, fragmented output records become overdue while their real475 // Fresh, fragmented output records become overdue while their real
411 // cumulative ACKs are lost. Disabling pacing intentionally forces overload476 // cumulative ACKs are lost. Disabling pacing intentionally forces overload
@@ -414,48 +479,60 @@ func TestTransportFloodClassification(t *testing.T) {
414 awaitSignal(t, clk.timerCreated, "ACK coalescing timer creation")479 awaitSignal(t, clk.timerCreated, "ACK coalescing timer creation")
415 clk.advance(maxACKDelay)480 clk.advance(maxACKDelay)
416 awaitSignal(t, acksDropped, "dropped cumulative ACK")481 awaitSignal(t, acksDropped, "dropped cumulative ACK")
482+ a.mu.Lock()
483+ a.rto = 10 * time.Millisecond
484+ a.mu.Unlock()
485+ a.queueRetransmits()
486+ a.mu.Lock()
487+ a.rto = time.Hour
488+ a.mu.Unlock()
489+ clk.advance(initialPacingRTT)
417 490
418 awaitSignal(t, retransmitBlocked, "retransmit pacing barrier")491 awaitSignal(t, retransmitBlocked, "retransmit pacing barrier")
419 if state := floodState(a, b, nil); state.retransmits < floodRecordCount {492 if state := floodState(a, b, nil); state.retransmits < floodRecordCount {
420 t.Fatalf("flood state = %+v, want all %d overdue records selected", state, floodRecordCount)493 t.Fatalf("flood state = %+v, want all %d overdue records selected", state, floodRecordCount)
421 }494 }
422-495+ select {
423- // Start a third large output at this same fake-clock instant. The pacing496+ case <-probeSent:
424- // barrier proves its real Send reaches the shared pacing gate while the497+ default:
425- // retransmit owns it inside WriteTo.
426- a.mu.Lock()
427- a.outputNext = time.Time{}
428- a.beforeDataPace = func() {
429- close(freshAtPace)
430- awaitSignal(t, allowFreshPace, "fresh pacing release")
431 }498 }
432- a.mu.Unlock()499+
500+ // A fresh record queues behind the sole data sender while control traffic
501+ // remains independent of congestion tokens and the blocked data write.
433 go func() {502 go func() {
434 freshReturned <- a.Send(ports.Frame{Type: ports.MsgOutput, Payload: floodOutputPayload(floodRecordCount, 128)})503 freshReturned <- a.Send(ports.Frame{Type: ports.MsgOutput, Payload: floodOutputPayload(floodRecordCount, 128)})
435 }()504 }()
436- awaitSignal(t, freshAtPace, "fresh output pacing barrier")
437- a.mu.Lock()
438- a.beforeDataPace = nil
439- a.mu.Unlock()
440- close(allowFreshPace)
441 select {505 select {
442 case err := <-freshReturned:506 case err := <-freshReturned:
443 t.Fatalf("fresh output completed while retransmit held pacing: %v", err)507 t.Fatalf("fresh output completed while retransmit held pacing: %v", err)
444 default:508 default:
445 }509 }
446 510
511+ clk.advance(11 * time.Millisecond)
447 awaitSignal(t, probeSent, "heartbeat probe during output contention")512 awaitSignal(t, probeSent, "heartbeat probe during output contention")
448 513
449 close(releaseRetransmit)514 close(releaseRetransmit)
450- if err := awaitResult(t, freshReturned, "fresh output completion"); err != nil {515+ for range 1024 {
451- t.Fatal(err)516+ clk.advance(initialPacingRTT)
517+ time.Sleep(time.Millisecond)
518+ select {
519+ case err := <-freshReturned:
520+ if err != nil {
521+ t.Fatal(err)
522+ }
523+ awaitSignal(t, lastRetransmitted, "last overdue record retransmission")
524+ writesMu.Lock()
525+ defer writesMu.Unlock()
526+ if writes[1] < 2 || writes[floodRecordCount] < 2 || writes[floodRecordCount+1] == 0 {
527+ t.Fatalf("flood state = %+v, writes=%v, want fresh large traffic and retransmissions across %d overdue records", floodState(a, b, nil), writes, floodRecordCount)
528+ }
529+ return
530+ default:
531+ }
452 }532 }
453- awaitSignal(t, lastRetransmitted, "last overdue record retransmission")
454 writesMu.Lock()533 writesMu.Lock()
455 defer writesMu.Unlock()534 defer writesMu.Unlock()
456- if writes[1] < 2 || writes[floodRecordCount] < 2 || writes[floodRecordCount+1] == 0 {535+ t.Fatalf("fresh output did not complete after paced retransmissions: writes=%v state=%+v", writes, floodState(a, b, nil))
457- t.Fatalf("flood state = %+v, writes=%v, want fresh large traffic and retransmissions across %d overdue records", floodState(a, b, nil), writes, floodRecordCount)
458- }
459 })536 })
460 }537 }
461 538
@@ -510,23 +587,37 @@ func TestFloodOutputPayloadIsIncrementalStateBearingOutput(t *testing.T) {
510 587
511 func sendFloodOutputs(t *testing.T, sender *Transport, count, mtu int) {588 func sendFloodOutputs(t *testing.T, sender *Transport, count, mtu int) {
512 t.Helper()589 t.Helper()
513- sender.dataPaceMu.Lock()
514- sender.dataPaceRemaining = count * floodRecordMTUs * 4
515- sender.dataPaceNext = sender.clock.Now().Add(time.Hour)
516- sender.dataPaceMu.Unlock()
517 for state := range count {590 for state := range count {
591+ if err := sender.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: floodOutputPayload(state, mtu)}); err != nil {
592+ t.Fatal(err)
593+ }
594+ }
595+ clk, ok := sender.clock.(*manualClock)
596+ if !ok {
597+ return
598+ }
599+ for range count * floodRecordMTUs * 2 {
600+ clk.advance(initialPacingRTT)
601+ time.Sleep(time.Millisecond)
518 sender.mu.Lock()602 sender.mu.Lock()
519- sender.outputNext = time.Time{}603+ complete := len(sender.outputQueue) == 0 && len(sender.dataSend) == 0
604+ for _, p := range sender.pending {
605+ complete = complete && !p.initialInFlight && !p.last.IsZero()
606+ }
520 sender.mu.Unlock()607 sender.mu.Unlock()
521- if err := sender.Send(ports.Frame{Type: ports.MsgOutput, Payload: floodOutputPayload(state, mtu)}); err != nil {608+ if complete {
522- t.Fatal(err)609+ return
523 }610 }
524 }611 }
612+ t.Fatal("flood output pacing did not drain")
525 }613 }
526 614
527 func newFloodTransports(t *testing.T, aPC, bPC *fakePC, clk *manualClock, mtu int) (*Transport, *Transport) {615 func newFloodTransports(t *testing.T, aPC, bPC *fakePC, clk *manualClock, mtu int) (*Transport, *Transport) {
528 t.Helper()616 t.Helper()
529- opts := Options{Clock: clk, MTU: mtu, ResendAfter: 10 * time.Millisecond, Heartbeat: time.Hour}617+ opts := Options{
618+ Clock: clk, MTU: mtu, ResendAfter: time.Hour, Heartbeat: time.Hour,
619+ DegradedAfter: time.Hour, ProbeAfter: time.Hour, OfflineAfter: time.Hour, DeadAfter: time.Hour,
620+ }
530 a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, opts)621 a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, opts)
531 if err != nil {622 if err != nil {
532 t.Fatal(err)623 t.Fatal(err)
M internal/adapters/dgram/output_pacer.go +11 -73
@@ -1,18 +1,16 @@
11 package dgram
22
3-import (
4- "time"
3+import "github.com/bnema/vev/internal/ports"
54
6- "github.com/bnema/vev/internal/ports"
7-)
8-
9-type queuedSend struct {
5+type dataSendJob struct {
106 seq uint64
117 reliable bool
128 frame ports.Frame
139 done chan error
1410 }
1511
12+type queuedSend = dataSendJob
13+
1614 func (t *Transport) outputPaceLoop() {
1715 for {
1816 t.outboundMu.Lock()
@@ -33,45 +31,14 @@ func (t *Transport) outputPaceLoop() {
3331 t.outboundMu.Unlock()
3432 return
3533 }
36- now := t.clock.Now()
37- wait := t.outputNext.Sub(now)
38- if wait > 0 {
39- wake := t.outputWake
40- timer := t.clock.NewTimer(wait)
41- t.mu.Unlock()
42- t.outboundMu.Unlock()
43- select {
44- case <-timer.C():
45- case <-wake:
46- case <-t.done:
47- timer.Stop()
48- return
49- }
50- timer.Stop()
51- continue
52- }
53- limit := 0
54- budget := t.mtu
55- for limit < len(t.outputQueue) && limit < defaultOutputPaceBatch {
56- cost := len(t.outputQueue[limit].frame.Payload) + dataRecordHeaderSize
57- if limit > 0 && cost > budget {
58- break
59- }
60- limit++
61- budget -= min(cost, budget)
62- }
63- batch := append([]queuedSend(nil), t.outputQueue[:limit]...)
64- copy(t.outputQueue, t.outputQueue[limit:])
65- t.outputQueue = t.outputQueue[:len(t.outputQueue)-limit]
66- t.outputNext = now.Add(t.outputPaceDelayLocked())
34+ q := t.outputQueue[0]
35+ copy(t.outputQueue, t.outputQueue[1:])
36+ t.outputQueue = t.outputQueue[:len(t.outputQueue)-1]
6737 t.mu.Unlock()
68- for i, q := range batch {
69- if err := t.sendQueuedData(q); err != nil {
70- t.removeQueuedPending(batch[i+1:], err)
71- t.closeWithError(err)
72- t.outboundMu.Unlock()
73- return
74- }
38+ if err := t.queueDataJob(dataSendJob(q)); err != nil {
39+ t.removeQueuedPending([]queuedSend{q}, err)
40+ t.outboundMu.Unlock()
41+ return
7542 }
7643 t.outboundMu.Unlock()
7744 }
@@ -85,39 +52,10 @@ func (t *Transport) notifyOutputPacerLocked() {
8552 t.outputWake = make(chan struct{})
8653 }
8754
88-func (t *Transport) outputPaceDelayLocked() time.Duration {
89- if t.srtt <= 0 {
90- return defaultOutputPaceMinDelay
91- }
92- d := t.srtt / 2
93- if d < defaultOutputPaceMinCadence {
94- d = defaultOutputPaceMinCadence
95- }
96- if d > defaultOutputPaceMaxDelay {
97- d = defaultOutputPaceMaxDelay
98- }
99- return d
100-}
101-
10255 func shouldPaceOutput(f ports.Frame) bool {
10356 return f.Type == ports.MsgOutput
10457 }
10558
106-func (t *Transport) sendQueuedData(q queuedSend) error {
107- t.markPendingSent(q.seq, q.reliable)
108- err := t.sendData(q.seq, q.reliable, q.frame)
109- if err != nil {
110- t.removePending(q.seq, q.reliable)
111- } else {
112- t.markPendingReady(q.seq, q.reliable)
113- }
114- if q.done != nil {
115- q.done <- err
116- close(q.done)
117- }
118- return err
119-}
120-
12159 func (t *Transport) markPendingSent(seq uint64, reliable bool) {
12260 if !reliable {
12361 return
@@ -1,18 +1,16 @@
1 package dgram1 package dgram
2 2
3-import (3+import "github.com/bnema/vev/internal/ports"
4- "time"
5 4
6- "github.com/bnema/vev/internal/ports"5+type dataSendJob struct {
7-)
8-
9-type queuedSend struct {
10 seq uint646 seq uint64
11 reliable bool7 reliable bool
12 frame ports.Frame8 frame ports.Frame
13 done chan error9 done chan error
14 }10 }
15 11
12+type queuedSend = dataSendJob
13+
16 func (t *Transport) outputPaceLoop() {14 func (t *Transport) outputPaceLoop() {
17 for {15 for {
18 t.outboundMu.Lock()16 t.outboundMu.Lock()
@@ -33,45 +31,14 @@ func (t *Transport) outputPaceLoop() {
33 t.outboundMu.Unlock()31 t.outboundMu.Unlock()
34 return32 return
35 }33 }
36- now := t.clock.Now()34+ q := t.outputQueue[0]
37- wait := t.outputNext.Sub(now)35+ copy(t.outputQueue, t.outputQueue[1:])
38- if wait > 0 {36+ t.outputQueue = t.outputQueue[:len(t.outputQueue)-1]
39- wake := t.outputWake
40- timer := t.clock.NewTimer(wait)
41- t.mu.Unlock()
42- t.outboundMu.Unlock()
43- select {
44- case <-timer.C():
45- case <-wake:
46- case <-t.done:
47- timer.Stop()
48- return
49- }
50- timer.Stop()
51- continue
52- }
53- limit := 0
54- budget := t.mtu
55- for limit < len(t.outputQueue) && limit < defaultOutputPaceBatch {
56- cost := len(t.outputQueue[limit].frame.Payload) + dataRecordHeaderSize
57- if limit > 0 && cost > budget {
58- break
59- }
60- limit++
61- budget -= min(cost, budget)
62- }
63- batch := append([]queuedSend(nil), t.outputQueue[:limit]...)
64- copy(t.outputQueue, t.outputQueue[limit:])
65- t.outputQueue = t.outputQueue[:len(t.outputQueue)-limit]
66- t.outputNext = now.Add(t.outputPaceDelayLocked())
67 t.mu.Unlock()37 t.mu.Unlock()
68- for i, q := range batch {38+ if err := t.queueDataJob(dataSendJob(q)); err != nil {
69- if err := t.sendQueuedData(q); err != nil {39+ t.removeQueuedPending([]queuedSend{q}, err)
70- t.removeQueuedPending(batch[i+1:], err)40+ t.outboundMu.Unlock()
71- t.closeWithError(err)41+ return
72- t.outboundMu.Unlock()
73- return
74- }
75 }42 }
76 t.outboundMu.Unlock()43 t.outboundMu.Unlock()
77 }44 }
@@ -85,39 +52,10 @@ func (t *Transport) notifyOutputPacerLocked() {
85 t.outputWake = make(chan struct{})52 t.outputWake = make(chan struct{})
86 }53 }
87 54
88-func (t *Transport) outputPaceDelayLocked() time.Duration {
89- if t.srtt <= 0 {
90- return defaultOutputPaceMinDelay
91- }
92- d := t.srtt / 2
93- if d < defaultOutputPaceMinCadence {
94- d = defaultOutputPaceMinCadence
95- }
96- if d > defaultOutputPaceMaxDelay {
97- d = defaultOutputPaceMaxDelay
98- }
99- return d
100-}
101-
102 func shouldPaceOutput(f ports.Frame) bool {55 func shouldPaceOutput(f ports.Frame) bool {
103 return f.Type == ports.MsgOutput56 return f.Type == ports.MsgOutput
104 }57 }
105 58
106-func (t *Transport) sendQueuedData(q queuedSend) error {
107- t.markPendingSent(q.seq, q.reliable)
108- err := t.sendData(q.seq, q.reliable, q.frame)
109- if err != nil {
110- t.removePending(q.seq, q.reliable)
111- } else {
112- t.markPendingReady(q.seq, q.reliable)
113- }
114- if q.done != nil {
115- q.done <- err
116- close(q.done)
117- }
118- return err
119-}
120-
121 func (t *Transport) markPendingSent(seq uint64, reliable bool) {59 func (t *Transport) markPendingSent(seq uint64, reliable bool) {
122 if !reliable {60 if !reliable {
123 return61 return
A internal/adapters/dgram/pacer.go +60 -0
@@ -0,0 +1,60 @@
1+package dgram
2+
3+import (
4+ "errors"
5+ "time"
6+
7+ "github.com/bnema/vev/internal/ports"
8+)
9+
10+var errPacerClosed = errors.New("dgram: pacer closed")
11+
12+type paceLimits func() (bytesPerSecond int, burstBytes int)
13+
14+type bytePacer struct {
15+ clk ports.Clock
16+ tokens int64
17+ last time.Time
18+}
19+
20+func (p *bytePacer) wait(done <-chan struct{}, n int, limits paceLimits) error {
21+ if n <= 0 {
22+ return nil
23+ }
24+ for {
25+ now := p.clk.Now()
26+ rate, burst := limits()
27+ if rate < 1 {
28+ rate = 1
29+ }
30+ if burst < n {
31+ burst = n
32+ }
33+ if p.last.IsZero() {
34+ p.last = now
35+ p.tokens = int64(burst)
36+ } else {
37+ elapsed := now.Sub(p.last)
38+ if elapsed > 0 {
39+ p.tokens += int64(elapsed) * int64(rate) / int64(time.Second)
40+ p.last = now
41+ }
42+ p.tokens = min(p.tokens, int64(burst))
43+ }
44+ if p.tokens >= int64(n) {
45+ p.tokens -= int64(n)
46+ return nil
47+ }
48+
49+ deficit := int64(n) - p.tokens
50+ wait := time.Duration((deficit*int64(time.Second) + int64(rate) - 1) / int64(rate))
51+ timer := p.clk.NewTimer(wait)
52+ select {
53+ case <-timer.C():
54+ timer.Stop()
55+ case <-done:
56+ timer.Stop()
57+ return errPacerClosed
58+ }
59+ }
60+}
@@ -0,0 +1,60 @@
1+package dgram
2+
3+import (
4+ "errors"
5+ "time"
6+
7+ "github.com/bnema/vev/internal/ports"
8+)
9+
10+var errPacerClosed = errors.New("dgram: pacer closed")
11+
12+type paceLimits func() (bytesPerSecond int, burstBytes int)
13+
14+type bytePacer struct {
15+ clk ports.Clock
16+ tokens int64
17+ last time.Time
18+}
19+
20+func (p *bytePacer) wait(done <-chan struct{}, n int, limits paceLimits) error {
21+ if n <= 0 {
22+ return nil
23+ }
24+ for {
25+ now := p.clk.Now()
26+ rate, burst := limits()
27+ if rate < 1 {
28+ rate = 1
29+ }
30+ if burst < n {
31+ burst = n
32+ }
33+ if p.last.IsZero() {
34+ p.last = now
35+ p.tokens = int64(burst)
36+ } else {
37+ elapsed := now.Sub(p.last)
38+ if elapsed > 0 {
39+ p.tokens += int64(elapsed) * int64(rate) / int64(time.Second)
40+ p.last = now
41+ }
42+ p.tokens = min(p.tokens, int64(burst))
43+ }
44+ if p.tokens >= int64(n) {
45+ p.tokens -= int64(n)
46+ return nil
47+ }
48+
49+ deficit := int64(n) - p.tokens
50+ wait := time.Duration((deficit*int64(time.Second) + int64(rate) - 1) / int64(rate))
51+ timer := p.clk.NewTimer(wait)
52+ select {
53+ case <-timer.C():
54+ timer.Stop()
55+ case <-done:
56+ timer.Stop()
57+ return errPacerClosed
58+ }
59+ }
60+}
M internal/adapters/dgram/transport.go +162 -135
@@ -20,28 +20,23 @@ const (
2020 recProbe byte = 3
2121 recPong byte = 4
2222
23- defaultMTU = pdgram.DefaultMTU
24- defaultResend = 250 * time.Millisecond
25- defaultMaxResendAfter = 2 * time.Second
26- defaultMaxResendPerTick = 64
27- defaultWriteTimeout = 250 * time.Millisecond
28- defaultHeartbeat = 3 * time.Second
29- defaultDegraded = 10 * time.Second
30- defaultProbe = 20 * time.Second
31- defaultOffline = 30 * time.Second
32- defaultDead = 60 * time.Second
33- defaultMaxPending = 1024
34- defaultMaxPendingWait = 50 * time.Millisecond
35- defaultMaxRecvBuf = 1024
36- defaultOutputPaceMinDelay = 8 * time.Millisecond
37- defaultOutputPaceMinCadence = 20 * time.Millisecond
38- defaultOutputPaceMaxDelay = 250 * time.Millisecond
39- defaultOutputPaceBatch = 2
40- defaultPacketPaceBudget = 8
41- dataRecordHeaderSize = 12
42- maxRecvBuffer = defaultMaxRecvBuf
43- linkEventBufferSize = 16
44- probeReplyBufferSize = controlQueueSize // legacy test burst bound
23+ defaultMTU = pdgram.DefaultMTU
24+ defaultResend = 250 * time.Millisecond
25+ defaultMaxResendAfter = 2 * time.Second
26+ defaultMaxResendPerTick = 64
27+ defaultWriteTimeout = 250 * time.Millisecond
28+ defaultHeartbeat = 3 * time.Second
29+ defaultDegraded = 10 * time.Second
30+ defaultProbe = 20 * time.Second
31+ defaultOffline = 30 * time.Second
32+ defaultDead = 60 * time.Second
33+ defaultMaxPending = 1024
34+ defaultMaxPendingWait = 50 * time.Millisecond
35+ defaultMaxRecvBuf = 1024
36+ dataRecordHeaderSize = 12
37+ maxRecvBuffer = defaultMaxRecvBuf
38+ linkEventBufferSize = 16
39+ probeReplyBufferSize = controlQueueSize // legacy test burst bound
4540 )
4641
4742 var (
@@ -188,6 +183,7 @@ type Transport struct {
188183 maxPendingWait time.Duration
189184 maxRecvBuffer int
190185 clock ports.Clock
186+ congestion congestionController
191187 linkState ports.LinkState
192188 linkStateGeneration uint64
193189 linkEvents chan ports.LinkEvent
@@ -205,7 +201,7 @@ type Transport struct {
205201 hopGeneration uint64
206202 outputQueue []queuedSend
207203 outputWake chan struct{}
208- outputNext time.Time
204+ dataSend chan dataSendJob
209205 retransmitWork chan []retransmitRecord
210206 writeDeadlines *writeDeadlineState
211207
@@ -215,10 +211,7 @@ type Transport struct {
215211 in chan ports.Frame
216212 done chan struct{}
217213
218- writeMu sync.Mutex
219- dataPaceMu sync.Mutex
220- dataPaceRemaining int
221- dataPaceNext time.Time
214+ writeMu sync.Mutex
222215 // beforeDataPace is a test synchronization hook. It is read under mu.
223216 beforeDataPace func()
224217 // outboundMu orders first transmissions and paced batches. Retransmits use
@@ -239,6 +232,7 @@ type pending struct {
239232 initialInFlight bool
240233 attempts int
241234 retransmitted bool
235+ wireBytes int
242236 }
243237
244238 type retransmitRecord struct {
@@ -338,6 +332,7 @@ func NewTransportWithOptions(pc net.PacketConn, peer net.Addr, key []byte, sendD
338332 maxPendingWait: opts.MaxPendingWait,
339333 maxRecvBuffer: opts.MaxRecvBuffer,
340334 clock: opts.Clock,
335+ congestion: newCongestionController(opts.MTU),
341336 linkState: ports.LinkStateConnected,
342337 linkEvents: make(chan ports.LinkEvent, linkEventBufferSize),
343338 probeWait: make(map[uint64]chan struct{}),
@@ -350,6 +345,7 @@ func NewTransportWithOptions(pc net.PacketConn, peer net.Addr, key []byte, sendD
350345 t.ackWake = make(chan struct{}, 1)
351346 t.ackSend = make(chan uint64, 1)
352347 t.outputWake = make(chan struct{})
348+ t.dataSend = make(chan dataSendJob, opts.MaxPending)
353349 t.writeDeadlines = newWriteDeadlineState(pc)
354350 // One queued batch bounds retransmit work while its sole sender is paced.
355351 t.retransmitWork = make(chan []retransmitRecord, 1)
@@ -362,7 +358,7 @@ func NewTransportWithOptions(pc net.PacketConn, peer net.Addr, key []byte, sendD
362358 go t.retransmitLoop()
363359 go t.heartbeatLoop()
364360 go t.healthLoop()
365- go t.retransmitSenderLoop()
361+ go t.dataSendLoop()
366362 go t.deliveryLoop()
367363 go t.controlLoop()
368364 go t.ackSendLoop()
@@ -399,60 +395,47 @@ func (t *Transport) send(f ports.Frame, async bool) error {
399395 if len(t.pending) == 0 {
400396 t.health.pendingStarted(now)
401397 }
402- t.pending[seq] = &pending{frame: f, enqueued: now}
398+ t.pending[seq] = &pending{frame: f, enqueued: now, wireBytes: dataRecordHeaderSize + len(f.Payload)}
403399 }
404- if shouldPaceOutput(f) {
405- now := t.clock.Now()
406- if len(t.outputQueue) > 0 || (!t.outputNext.IsZero() && now.Before(t.outputNext)) {
407- wasEmpty := len(t.outputQueue) == 0
408- var done chan error
409- if !async {
410- done = make(chan error, 1)
411- }
412- t.outputQueue = append(t.outputQueue, queuedSend{seq: seq, reliable: reliable, frame: f, done: done})
413- if wasEmpty {
414- t.notifyOutputPacerLocked()
415- }
416- t.mu.Unlock()
417- if done == nil {
418- return nil
419- }
420- t.outboundMu.Unlock()
421- unlockOutbound = false
422- return t.waitQueuedSend(done)
423- }
424- t.outputNext = now.Add(t.outputPaceDelayLocked())
425- t.mu.Unlock()
426- t.markPendingSent(seq, reliable)
427- if err := t.sendData(seq, reliable, f); err != nil {
428- t.removePending(seq, reliable)
429- return err
430- }
431- t.markPendingReady(seq, reliable)
432- return nil
400+ var done chan error
401+ if !async {
402+ done = make(chan error, 1)
433403 }
434- if len(t.outputQueue) > 0 {
435- var done chan error
436- if !async {
437- done = make(chan error, 1)
404+ job := dataSendJob{seq: seq, reliable: reliable, frame: f, done: done}
405+ if shouldPaceOutput(f) || len(t.outputQueue) > 0 {
406+ wasEmpty := len(t.outputQueue) == 0
407+ t.outputQueue = append(t.outputQueue, queuedSend(job))
408+ if wasEmpty {
409+ t.notifyOutputPacerLocked()
438410 }
439- t.outputQueue = append(t.outputQueue, queuedSend{seq: seq, reliable: reliable, frame: f, done: done})
440411 t.mu.Unlock()
441- if done == nil {
442- return nil
443- }
444412 t.outboundMu.Unlock()
445413 unlockOutbound = false
414+ if async {
415+ return nil
416+ }
446417 return t.waitQueuedSend(done)
447418 }
448419 t.mu.Unlock()
449- t.markPendingSent(seq, reliable)
450- if err := t.sendData(seq, reliable, f); err != nil {
420+ if err := t.queueDataJob(job); err != nil {
451421 t.removePending(seq, reliable)
452422 return err
453423 }
454- t.markPendingReady(seq, reliable)
455- return nil
424+ t.outboundMu.Unlock()
425+ unlockOutbound = false
426+ if async {
427+ return nil
428+ }
429+ return t.waitQueuedSend(done)
430+}
431+
432+func (t *Transport) queueDataJob(job dataSendJob) error {
433+ select {
434+ case t.dataSend <- job:
435+ return nil
436+ case <-t.done:
437+ return t.closedError()
438+ }
456439 }
457440
458441 func (t *Transport) waitQueuedSend(done <-chan error) error {
@@ -653,40 +636,18 @@ func (t *Transport) unregisterProbe(id uint64) {
653636 t.mu.Unlock()
654637 }
655638
656-func (t *Transport) sendData(seq uint64, reliable bool, f ports.Frame) error {
657- p := encodeData(seq, reliable, f)
658- return t.sendPayload(p, true, seq)
659-}
660639 func (t *Transport) sendAck(seq uint64) {
661640 var b [9]byte
662641 b[0] = recAck
663642 binary.BigEndian.PutUint64(b[1:], seq)
664- _ = t.sendPayload(b[:], false, 0)
643+ _ = t.sendPayload(b[:])
665644 }
666-func (t *Transport) sendPayload(p []byte, paced bool, pendingSeq uint64) error {
667- if paced {
668- t.mu.Lock()
669- beforeDataPace := t.beforeDataPace
670- t.mu.Unlock()
671- if beforeDataPace != nil {
672- beforeDataPace()
673- }
674- t.dataPaceMu.Lock()
675- defer t.dataPaceMu.Unlock()
676- }
645+func (t *Transport) sendPayload(p []byte) error {
677646 frags, err := pdgram.FragmentPayload(t.nextCounter(), p, t.mtu-pdgram.HeaderSize-t.codec.Overhead())
678647 if err != nil {
679648 return err
680649 }
681- for i, f := range frags {
682- if pendingSeq != 0 && !t.pendingExists(pendingSeq) {
683- return nil
684- }
685- if paced {
686- if err := t.takeDataPaceSlot(); err != nil {
687- return err
688- }
689- }
650+ for _, f := range frags {
690651 raw, err := pdgram.MarshalFragment(f)
691652 if err != nil {
692653 return err
@@ -706,9 +667,6 @@ func (t *Transport) sendPayload(p []byte, paced bool, pendingSeq uint64) error {
706667 if peer == nil {
707668 return errors.New("dgram: no peer")
708669 }
709- if pendingSeq != 0 && i == len(frags)-1 {
710- t.markPendingFinalWrite(pendingSeq)
711- }
712670 if err := t.writePacket(pkt, peer); err != nil {
713671 return err
714672 }
@@ -723,30 +681,6 @@ func (t *Transport) pendingExists(seq uint64) bool {
723681 return ok
724682 }
725683
726-func (t *Transport) takeDataPaceSlot() error {
727- now := t.clock.Now()
728- if t.dataPaceRemaining > 0 && now.Before(t.dataPaceNext) {
729- t.dataPaceRemaining--
730- return nil
731- }
732- if t.dataPaceNext.After(now) {
733- timer := t.clock.NewTimer(t.dataPaceNext.Sub(now))
734- select {
735- case <-timer.C():
736- case <-t.done:
737- timer.Stop()
738- return t.closedError()
739- }
740- timer.Stop()
741- }
742- t.mu.Lock()
743- delay := t.outputPaceDelayLocked()
744- t.mu.Unlock()
745- t.dataPaceRemaining = defaultPacketPaceBudget - 1
746- t.dataPaceNext = t.clock.Now().Add(delay)
747- return nil
748-}
749-
750684 func (t *Transport) writePacket(pkt []byte, peer net.Addr) error {
751685 t.writeMu.Lock()
752686 defer t.writeMu.Unlock()
@@ -878,6 +812,7 @@ func (t *Transport) handleRecord(p []byte) {
878812 seq := binary.BigEndian.Uint64(p[1:])
879813 t.mu.Lock()
880814 acked := false
815+ ackedBytes := 0
881816 for pendingSeq, p := range t.pending {
882817 if pendingSeq > seq {
883818 continue
@@ -885,10 +820,14 @@ func (t *Transport) handleRecord(p []byte) {
885820 if pendingSeq == seq && p != nil && !p.retransmitted {
886821 t.updateRTTLocked(t.clock.Now().Sub(p.first))
887822 }
823+ if p != nil {
824+ ackedBytes += p.wireBytes
825+ }
888826 delete(t.pending, pendingSeq)
889827 acked = true
890828 }
891829 if acked {
830+ t.congestion.onACK(ackedBytes)
892831 now := t.clock.Now()
893832 t.health.ackProgress(now)
894833 if len(t.pending) == 0 {
@@ -1042,20 +981,15 @@ func (t *Transport) healthLoop() {
1042981 }
1043982 }
1044983
1045-func (t *Transport) retransmitSenderLoop() {
984+func (t *Transport) dataSendLoop() {
985+ pacer := bytePacer{clk: t.clock}
1046986 for {
1047987 select {
988+ case job := <-t.dataSend:
989+ t.runDataSendJob(&pacer, job, true)
1048990 case batch := <-t.retransmitWork:
1049991 for _, r := range batch {
1050- if err := t.sendData(r.seq, true, r.f); err == nil {
1051- t.markPendingReady(r.seq, true)
1052- } else {
1053- t.mu.Lock()
1054- if p := t.pending[r.seq]; p != nil {
1055- p.initialInFlight = false
1056- }
1057- t.mu.Unlock()
1058- }
992+ t.runDataSendJob(&pacer, dataSendJob{seq: r.seq, reliable: true, frame: r.f}, false)
1059993 }
1060994 case <-t.done:
1061995 return
@@ -1063,6 +997,88 @@ func (t *Transport) retransmitSenderLoop() {
1063997 }
1064998 }
1065999
1000+func (t *Transport) runDataSendJob(pacer *bytePacer, job dataSendJob, initial bool) {
1001+ if initial {
1002+ t.markPendingSent(job.seq, job.reliable)
1003+ }
1004+ err := t.writeDataJob(pacer, job)
1005+ if err != nil {
1006+ if initial {
1007+ t.removePending(job.seq, job.reliable)
1008+ } else {
1009+ t.markPendingReady(job.seq, job.reliable)
1010+ }
1011+ if !errors.Is(err, errPacerClosed) {
1012+ t.closeWithError(err)
1013+ }
1014+ } else {
1015+ t.markPendingReady(job.seq, job.reliable)
1016+ }
1017+ if job.done != nil {
1018+ if errors.Is(err, errPacerClosed) {
1019+ err = t.closedError()
1020+ }
1021+ job.done <- err
1022+ close(job.done)
1023+ }
1024+}
1025+
1026+func (t *Transport) writeDataJob(pacer *bytePacer, job dataSendJob) error {
1027+ payload := encodeData(job.seq, job.reliable, job.frame)
1028+ frags, err := pdgram.FragmentPayload(t.nextCounter(), payload, t.mtu-pdgram.HeaderSize-t.codec.Overhead())
1029+ if err != nil {
1030+ return err
1031+ }
1032+ t.mu.Lock()
1033+ beforeDataPace := t.beforeDataPace
1034+ t.mu.Unlock()
1035+ if beforeDataPace != nil {
1036+ beforeDataPace()
1037+ }
1038+ limits := func() (int, int) {
1039+ t.mu.Lock()
1040+ defer t.mu.Unlock()
1041+ return t.congestion.bytesPerSecond(), t.congestion.burstBytes()
1042+ }
1043+ for i, frag := range frags {
1044+ if job.reliable && !t.pendingExists(job.seq) {
1045+ return nil
1046+ }
1047+ raw, err := pdgram.MarshalFragment(frag)
1048+ if err != nil {
1049+ return err
1050+ }
1051+ pkt := t.codec.Seal(t.sendDir, t.nextCounter(), raw, nil)
1052+ if err := pacer.wait(t.done, len(pkt), limits); err != nil {
1053+ return err
1054+ }
1055+ if job.reliable && !t.pendingExists(job.seq) {
1056+ return nil
1057+ }
1058+ t.mu.Lock()
1059+ peer := t.peer
1060+ closed := t.closed
1061+ closeErr := t.closeErr
1062+ t.mu.Unlock()
1063+ if closed {
1064+ if closeErr != nil {
1065+ return closeErr
1066+ }
1067+ return net.ErrClosed
1068+ }
1069+ if peer == nil {
1070+ return errors.New("dgram: no peer")
1071+ }
1072+ if i == len(frags)-1 {
1073+ t.markPendingFinalWrite(job.seq)
1074+ }
1075+ if err := t.writePacket(pkt, peer); err != nil {
1076+ return err
1077+ }
1078+ }
1079+ return nil
1080+}
1081+
10661082 func (t *Transport) heartbeatLoop() {
10671083 heartbeat := t.clock.NewTimer(t.heartbeat)
10681084 defer heartbeat.Stop()
@@ -1125,6 +1141,7 @@ func (t *Transport) selectRetransmitsLocked(now time.Time) ([]retransmitRecord,
11251141 p.initialInFlight = true
11261142 p.attempts++
11271143 p.retransmitted = true
1144+ t.congestion.onLoss()
11281145 t.retransmits++
11291146 emitDiagnostic = emitDiagnostic || t.retransmits%64 == 0
11301147 resend = append(resend, retransmitRecord{seq: seq, f: p.frame})
@@ -1133,15 +1150,24 @@ func (t *Transport) selectRetransmitsLocked(now time.Time) ([]retransmitRecord,
11331150 return resend, emitDiagnostic
11341151 }
11351152
1136-// resendPending remains a synchronous test hook for retransmission selection
1137-// unit tests. Production timer paths use queueRetransmits and the sole sender.
1153+// resendPending remains a test hook for retransmission selection. Constructed
1154+// transports transfer its batch to the sole data sender; small unit fixtures
1155+// without transport goroutines execute the selected writes inline.
11381156 func (t *Transport) resendPending() {
11391157 now := t.clock.Now()
11401158 t.mu.Lock()
11411159 resend, _ := t.selectRetransmitsLocked(now)
11421160 t.mu.Unlock()
1161+ if t.retransmitWork != nil {
1162+ select {
1163+ case t.retransmitWork <- resend:
1164+ case <-t.done:
1165+ }
1166+ return
1167+ }
1168+ pacer := bytePacer{clk: t.clock}
11431169 for _, r := range resend {
1144- if err := t.sendData(r.seq, true, r.f); err == nil {
1170+ if err := t.writeDataJob(&pacer, dataSendJob{seq: r.seq, reliable: true, frame: r.f}); err == nil {
11451171 t.markPendingReady(r.seq, true)
11461172 } else {
11471173 t.mu.Lock()
@@ -1194,6 +1220,7 @@ func (t *Transport) updateRTTLocked(sample time.Duration) {
11941220 rto = t.maxResendAfter
11951221 }
11961222 t.rto = rto
1223+ t.congestion.onRTT(t.srtt)
11971224 }
11981225
11991226 func (t *Transport) hopPacketConnOnce(generation uint64) {
@@ -20,28 +20,23 @@ const (
20 recProbe byte = 320 recProbe byte = 3
21 recPong byte = 421 recPong byte = 4
22 22
23- defaultMTU = pdgram.DefaultMTU23+ defaultMTU = pdgram.DefaultMTU
24- defaultResend = 250 * time.Millisecond24+ defaultResend = 250 * time.Millisecond
25- defaultMaxResendAfter = 2 * time.Second25+ defaultMaxResendAfter = 2 * time.Second
26- defaultMaxResendPerTick = 6426+ defaultMaxResendPerTick = 64
27- defaultWriteTimeout = 250 * time.Millisecond27+ defaultWriteTimeout = 250 * time.Millisecond
28- defaultHeartbeat = 3 * time.Second28+ defaultHeartbeat = 3 * time.Second
29- defaultDegraded = 10 * time.Second29+ defaultDegraded = 10 * time.Second
30- defaultProbe = 20 * time.Second30+ defaultProbe = 20 * time.Second
31- defaultOffline = 30 * time.Second31+ defaultOffline = 30 * time.Second
32- defaultDead = 60 * time.Second32+ defaultDead = 60 * time.Second
33- defaultMaxPending = 102433+ defaultMaxPending = 1024
34- defaultMaxPendingWait = 50 * time.Millisecond34+ defaultMaxPendingWait = 50 * time.Millisecond
35- defaultMaxRecvBuf = 102435+ defaultMaxRecvBuf = 1024
36- defaultOutputPaceMinDelay = 8 * time.Millisecond36+ dataRecordHeaderSize = 12
37- defaultOutputPaceMinCadence = 20 * time.Millisecond37+ maxRecvBuffer = defaultMaxRecvBuf
38- defaultOutputPaceMaxDelay = 250 * time.Millisecond38+ linkEventBufferSize = 16
39- defaultOutputPaceBatch = 239+ probeReplyBufferSize = controlQueueSize // legacy test burst bound
40- defaultPacketPaceBudget = 8
41- dataRecordHeaderSize = 12
42- maxRecvBuffer = defaultMaxRecvBuf
43- linkEventBufferSize = 16
44- probeReplyBufferSize = controlQueueSize // legacy test burst bound
45 )40 )
46 41
47 var (42 var (
@@ -188,6 +183,7 @@ type Transport struct {
188 maxPendingWait time.Duration183 maxPendingWait time.Duration
189 maxRecvBuffer int184 maxRecvBuffer int
190 clock ports.Clock185 clock ports.Clock
186+ congestion congestionController
191 linkState ports.LinkState187 linkState ports.LinkState
192 linkStateGeneration uint64188 linkStateGeneration uint64
193 linkEvents chan ports.LinkEvent189 linkEvents chan ports.LinkEvent
@@ -205,7 +201,7 @@ type Transport struct {
205 hopGeneration uint64201 hopGeneration uint64
206 outputQueue []queuedSend202 outputQueue []queuedSend
207 outputWake chan struct{}203 outputWake chan struct{}
208- outputNext time.Time204+ dataSend chan dataSendJob
209 retransmitWork chan []retransmitRecord205 retransmitWork chan []retransmitRecord
210 writeDeadlines *writeDeadlineState206 writeDeadlines *writeDeadlineState
211 207
@@ -215,10 +211,7 @@ type Transport struct {
215 in chan ports.Frame211 in chan ports.Frame
216 done chan struct{}212 done chan struct{}
217 213
218- writeMu sync.Mutex214+ writeMu sync.Mutex
219- dataPaceMu sync.Mutex
220- dataPaceRemaining int
221- dataPaceNext time.Time
222 // beforeDataPace is a test synchronization hook. It is read under mu.215 // beforeDataPace is a test synchronization hook. It is read under mu.
223 beforeDataPace func()216 beforeDataPace func()
224 // outboundMu orders first transmissions and paced batches. Retransmits use217 // outboundMu orders first transmissions and paced batches. Retransmits use
@@ -239,6 +232,7 @@ type pending struct {
239 initialInFlight bool232 initialInFlight bool
240 attempts int233 attempts int
241 retransmitted bool234 retransmitted bool
235+ wireBytes int
242 }236 }
243 237
244 type retransmitRecord struct {238 type retransmitRecord struct {
@@ -338,6 +332,7 @@ func NewTransportWithOptions(pc net.PacketConn, peer net.Addr, key []byte, sendD
338 maxPendingWait: opts.MaxPendingWait,332 maxPendingWait: opts.MaxPendingWait,
339 maxRecvBuffer: opts.MaxRecvBuffer,333 maxRecvBuffer: opts.MaxRecvBuffer,
340 clock: opts.Clock,334 clock: opts.Clock,
335+ congestion: newCongestionController(opts.MTU),
341 linkState: ports.LinkStateConnected,336 linkState: ports.LinkStateConnected,
342 linkEvents: make(chan ports.LinkEvent, linkEventBufferSize),337 linkEvents: make(chan ports.LinkEvent, linkEventBufferSize),
343 probeWait: make(map[uint64]chan struct{}),338 probeWait: make(map[uint64]chan struct{}),
@@ -350,6 +345,7 @@ func NewTransportWithOptions(pc net.PacketConn, peer net.Addr, key []byte, sendD
350 t.ackWake = make(chan struct{}, 1)345 t.ackWake = make(chan struct{}, 1)
351 t.ackSend = make(chan uint64, 1)346 t.ackSend = make(chan uint64, 1)
352 t.outputWake = make(chan struct{})347 t.outputWake = make(chan struct{})
348+ t.dataSend = make(chan dataSendJob, opts.MaxPending)
353 t.writeDeadlines = newWriteDeadlineState(pc)349 t.writeDeadlines = newWriteDeadlineState(pc)
354 // One queued batch bounds retransmit work while its sole sender is paced.350 // One queued batch bounds retransmit work while its sole sender is paced.
355 t.retransmitWork = make(chan []retransmitRecord, 1)351 t.retransmitWork = make(chan []retransmitRecord, 1)
@@ -362,7 +358,7 @@ func NewTransportWithOptions(pc net.PacketConn, peer net.Addr, key []byte, sendD
362 go t.retransmitLoop()358 go t.retransmitLoop()
363 go t.heartbeatLoop()359 go t.heartbeatLoop()
364 go t.healthLoop()360 go t.healthLoop()
365- go t.retransmitSenderLoop()361+ go t.dataSendLoop()
366 go t.deliveryLoop()362 go t.deliveryLoop()
367 go t.controlLoop()363 go t.controlLoop()
368 go t.ackSendLoop()364 go t.ackSendLoop()
@@ -399,60 +395,47 @@ func (t *Transport) send(f ports.Frame, async bool) error {
399 if len(t.pending) == 0 {395 if len(t.pending) == 0 {
400 t.health.pendingStarted(now)396 t.health.pendingStarted(now)
401 }397 }
402- t.pending[seq] = &pending{frame: f, enqueued: now}398+ t.pending[seq] = &pending{frame: f, enqueued: now, wireBytes: dataRecordHeaderSize + len(f.Payload)}
403 }399 }
404- if shouldPaceOutput(f) {400+ var done chan error
405- now := t.clock.Now()401+ if !async {
406- if len(t.outputQueue) > 0 || (!t.outputNext.IsZero() && now.Before(t.outputNext)) {402+ done = make(chan error, 1)
407- wasEmpty := len(t.outputQueue) == 0
408- var done chan error
409- if !async {
410- done = make(chan error, 1)
411- }
412- t.outputQueue = append(t.outputQueue, queuedSend{seq: seq, reliable: reliable, frame: f, done: done})
413- if wasEmpty {
414- t.notifyOutputPacerLocked()
415- }
416- t.mu.Unlock()
417- if done == nil {
418- return nil
419- }
420- t.outboundMu.Unlock()
421- unlockOutbound = false
422- return t.waitQueuedSend(done)
423- }
424- t.outputNext = now.Add(t.outputPaceDelayLocked())
425- t.mu.Unlock()
426- t.markPendingSent(seq, reliable)
427- if err := t.sendData(seq, reliable, f); err != nil {
428- t.removePending(seq, reliable)
429- return err
430- }
431- t.markPendingReady(seq, reliable)
432- return nil
433 }403 }
434- if len(t.outputQueue) > 0 {404+ job := dataSendJob{seq: seq, reliable: reliable, frame: f, done: done}
435- var done chan error405+ if shouldPaceOutput(f) || len(t.outputQueue) > 0 {
436- if !async {406+ wasEmpty := len(t.outputQueue) == 0
437- done = make(chan error, 1)407+ t.outputQueue = append(t.outputQueue, queuedSend(job))
408+ if wasEmpty {
409+ t.notifyOutputPacerLocked()
438 }410 }
439- t.outputQueue = append(t.outputQueue, queuedSend{seq: seq, reliable: reliable, frame: f, done: done})
440 t.mu.Unlock()411 t.mu.Unlock()
441- if done == nil {
442- return nil
443- }
444 t.outboundMu.Unlock()412 t.outboundMu.Unlock()
445 unlockOutbound = false413 unlockOutbound = false
414+ if async {
415+ return nil
416+ }
446 return t.waitQueuedSend(done)417 return t.waitQueuedSend(done)
447 }418 }
448 t.mu.Unlock()419 t.mu.Unlock()
449- t.markPendingSent(seq, reliable)420+ if err := t.queueDataJob(job); err != nil {
450- if err := t.sendData(seq, reliable, f); err != nil {
451 t.removePending(seq, reliable)421 t.removePending(seq, reliable)
452 return err422 return err
453 }423 }
454- t.markPendingReady(seq, reliable)424+ t.outboundMu.Unlock()
455- return nil425+ unlockOutbound = false
426+ if async {
427+ return nil
428+ }
429+ return t.waitQueuedSend(done)
430+}
431+
432+func (t *Transport) queueDataJob(job dataSendJob) error {
433+ select {
434+ case t.dataSend <- job:
435+ return nil
436+ case <-t.done:
437+ return t.closedError()
438+ }
456 }439 }
457 440
458 func (t *Transport) waitQueuedSend(done <-chan error) error {441 func (t *Transport) waitQueuedSend(done <-chan error) error {
@@ -653,40 +636,18 @@ func (t *Transport) unregisterProbe(id uint64) {
653 t.mu.Unlock()636 t.mu.Unlock()
654 }637 }
655 638
656-func (t *Transport) sendData(seq uint64, reliable bool, f ports.Frame) error {
657- p := encodeData(seq, reliable, f)
658- return t.sendPayload(p, true, seq)
659-}
660 func (t *Transport) sendAck(seq uint64) {639 func (t *Transport) sendAck(seq uint64) {
661 var b [9]byte640 var b [9]byte
662 b[0] = recAck641 b[0] = recAck
663 binary.BigEndian.PutUint64(b[1:], seq)642 binary.BigEndian.PutUint64(b[1:], seq)
664- _ = t.sendPayload(b[:], false, 0)643+ _ = t.sendPayload(b[:])
665 }644 }
666-func (t *Transport) sendPayload(p []byte, paced bool, pendingSeq uint64) error {645+func (t *Transport) sendPayload(p []byte) error {
667- if paced {
668- t.mu.Lock()
669- beforeDataPace := t.beforeDataPace
670- t.mu.Unlock()
671- if beforeDataPace != nil {
672- beforeDataPace()
673- }
674- t.dataPaceMu.Lock()
675- defer t.dataPaceMu.Unlock()
676- }
677 frags, err := pdgram.FragmentPayload(t.nextCounter(), p, t.mtu-pdgram.HeaderSize-t.codec.Overhead())646 frags, err := pdgram.FragmentPayload(t.nextCounter(), p, t.mtu-pdgram.HeaderSize-t.codec.Overhead())
678 if err != nil {647 if err != nil {
679 return err648 return err
680 }649 }
681- for i, f := range frags {650+ for _, f := range frags {
682- if pendingSeq != 0 && !t.pendingExists(pendingSeq) {
683- return nil
684- }
685- if paced {
686- if err := t.takeDataPaceSlot(); err != nil {
687- return err
688- }
689- }
690 raw, err := pdgram.MarshalFragment(f)651 raw, err := pdgram.MarshalFragment(f)
691 if err != nil {652 if err != nil {
692 return err653 return err
@@ -706,9 +667,6 @@ func (t *Transport) sendPayload(p []byte, paced bool, pendingSeq uint64) error {
706 if peer == nil {667 if peer == nil {
707 return errors.New("dgram: no peer")668 return errors.New("dgram: no peer")
708 }669 }
709- if pendingSeq != 0 && i == len(frags)-1 {
710- t.markPendingFinalWrite(pendingSeq)
711- }
712 if err := t.writePacket(pkt, peer); err != nil {670 if err := t.writePacket(pkt, peer); err != nil {
713 return err671 return err
714 }672 }
@@ -723,30 +681,6 @@ func (t *Transport) pendingExists(seq uint64) bool {
723 return ok681 return ok
724 }682 }
725 683
726-func (t *Transport) takeDataPaceSlot() error {
727- now := t.clock.Now()
728- if t.dataPaceRemaining > 0 && now.Before(t.dataPaceNext) {
729- t.dataPaceRemaining--
730- return nil
731- }
732- if t.dataPaceNext.After(now) {
733- timer := t.clock.NewTimer(t.dataPaceNext.Sub(now))
734- select {
735- case <-timer.C():
736- case <-t.done:
737- timer.Stop()
738- return t.closedError()
739- }
740- timer.Stop()
741- }
742- t.mu.Lock()
743- delay := t.outputPaceDelayLocked()
744- t.mu.Unlock()
745- t.dataPaceRemaining = defaultPacketPaceBudget - 1
746- t.dataPaceNext = t.clock.Now().Add(delay)
747- return nil
748-}
749-
750 func (t *Transport) writePacket(pkt []byte, peer net.Addr) error {684 func (t *Transport) writePacket(pkt []byte, peer net.Addr) error {
751 t.writeMu.Lock()685 t.writeMu.Lock()
752 defer t.writeMu.Unlock()686 defer t.writeMu.Unlock()
@@ -878,6 +812,7 @@ func (t *Transport) handleRecord(p []byte) {
878 seq := binary.BigEndian.Uint64(p[1:])812 seq := binary.BigEndian.Uint64(p[1:])
879 t.mu.Lock()813 t.mu.Lock()
880 acked := false814 acked := false
815+ ackedBytes := 0
881 for pendingSeq, p := range t.pending {816 for pendingSeq, p := range t.pending {
882 if pendingSeq > seq {817 if pendingSeq > seq {
883 continue818 continue
@@ -885,10 +820,14 @@ func (t *Transport) handleRecord(p []byte) {
885 if pendingSeq == seq && p != nil && !p.retransmitted {820 if pendingSeq == seq && p != nil && !p.retransmitted {
886 t.updateRTTLocked(t.clock.Now().Sub(p.first))821 t.updateRTTLocked(t.clock.Now().Sub(p.first))
887 }822 }
823+ if p != nil {
824+ ackedBytes += p.wireBytes
825+ }
888 delete(t.pending, pendingSeq)826 delete(t.pending, pendingSeq)
889 acked = true827 acked = true
890 }828 }
891 if acked {829 if acked {
830+ t.congestion.onACK(ackedBytes)
892 now := t.clock.Now()831 now := t.clock.Now()
893 t.health.ackProgress(now)832 t.health.ackProgress(now)
894 if len(t.pending) == 0 {833 if len(t.pending) == 0 {
@@ -1042,20 +981,15 @@ func (t *Transport) healthLoop() {
1042 }981 }
1043 }982 }
1044 983
1045-func (t *Transport) retransmitSenderLoop() {984+func (t *Transport) dataSendLoop() {
985+ pacer := bytePacer{clk: t.clock}
1046 for {986 for {
1047 select {987 select {
988+ case job := <-t.dataSend:
989+ t.runDataSendJob(&pacer, job, true)
1048 case batch := <-t.retransmitWork:990 case batch := <-t.retransmitWork:
1049 for _, r := range batch {991 for _, r := range batch {
1050- if err := t.sendData(r.seq, true, r.f); err == nil {992+ t.runDataSendJob(&pacer, dataSendJob{seq: r.seq, reliable: true, frame: r.f}, false)
1051- t.markPendingReady(r.seq, true)
1052- } else {
1053- t.mu.Lock()
1054- if p := t.pending[r.seq]; p != nil {
1055- p.initialInFlight = false
1056- }
1057- t.mu.Unlock()
1058- }
1059 }993 }
1060 case <-t.done:994 case <-t.done:
1061 return995 return
@@ -1063,6 +997,88 @@ func (t *Transport) retransmitSenderLoop() {
1063 }997 }
1064 }998 }
1065 999
1000+func (t *Transport) runDataSendJob(pacer *bytePacer, job dataSendJob, initial bool) {
1001+ if initial {
1002+ t.markPendingSent(job.seq, job.reliable)
1003+ }
1004+ err := t.writeDataJob(pacer, job)
1005+ if err != nil {
1006+ if initial {
1007+ t.removePending(job.seq, job.reliable)
1008+ } else {
1009+ t.markPendingReady(job.seq, job.reliable)
1010+ }
1011+ if !errors.Is(err, errPacerClosed) {
1012+ t.closeWithError(err)
1013+ }
1014+ } else {
1015+ t.markPendingReady(job.seq, job.reliable)
1016+ }
1017+ if job.done != nil {
1018+ if errors.Is(err, errPacerClosed) {
1019+ err = t.closedError()
1020+ }
1021+ job.done <- err
1022+ close(job.done)
1023+ }
1024+}
1025+
1026+func (t *Transport) writeDataJob(pacer *bytePacer, job dataSendJob) error {
1027+ payload := encodeData(job.seq, job.reliable, job.frame)
1028+ frags, err := pdgram.FragmentPayload(t.nextCounter(), payload, t.mtu-pdgram.HeaderSize-t.codec.Overhead())
1029+ if err != nil {
1030+ return err
1031+ }
1032+ t.mu.Lock()
1033+ beforeDataPace := t.beforeDataPace
1034+ t.mu.Unlock()
1035+ if beforeDataPace != nil {
1036+ beforeDataPace()
1037+ }
1038+ limits := func() (int, int) {
1039+ t.mu.Lock()
1040+ defer t.mu.Unlock()
1041+ return t.congestion.bytesPerSecond(), t.congestion.burstBytes()
1042+ }
1043+ for i, frag := range frags {
1044+ if job.reliable && !t.pendingExists(job.seq) {
1045+ return nil
1046+ }
1047+ raw, err := pdgram.MarshalFragment(frag)
1048+ if err != nil {
1049+ return err
1050+ }
1051+ pkt := t.codec.Seal(t.sendDir, t.nextCounter(), raw, nil)
1052+ if err := pacer.wait(t.done, len(pkt), limits); err != nil {
1053+ return err
1054+ }
1055+ if job.reliable && !t.pendingExists(job.seq) {
1056+ return nil
1057+ }
1058+ t.mu.Lock()
1059+ peer := t.peer
1060+ closed := t.closed
1061+ closeErr := t.closeErr
1062+ t.mu.Unlock()
1063+ if closed {
1064+ if closeErr != nil {
1065+ return closeErr
1066+ }
1067+ return net.ErrClosed
1068+ }
1069+ if peer == nil {
1070+ return errors.New("dgram: no peer")
1071+ }
1072+ if i == len(frags)-1 {
1073+ t.markPendingFinalWrite(job.seq)
1074+ }
1075+ if err := t.writePacket(pkt, peer); err != nil {
1076+ return err
1077+ }
1078+ }
1079+ return nil
1080+}
1081+
1066 func (t *Transport) heartbeatLoop() {1082 func (t *Transport) heartbeatLoop() {
1067 heartbeat := t.clock.NewTimer(t.heartbeat)1083 heartbeat := t.clock.NewTimer(t.heartbeat)
1068 defer heartbeat.Stop()1084 defer heartbeat.Stop()
@@ -1125,6 +1141,7 @@ func (t *Transport) selectRetransmitsLocked(now time.Time) ([]retransmitRecord,
1125 p.initialInFlight = true1141 p.initialInFlight = true
1126 p.attempts++1142 p.attempts++
1127 p.retransmitted = true1143 p.retransmitted = true
1144+ t.congestion.onLoss()
1128 t.retransmits++1145 t.retransmits++
1129 emitDiagnostic = emitDiagnostic || t.retransmits%64 == 01146 emitDiagnostic = emitDiagnostic || t.retransmits%64 == 0
1130 resend = append(resend, retransmitRecord{seq: seq, f: p.frame})1147 resend = append(resend, retransmitRecord{seq: seq, f: p.frame})
@@ -1133,15 +1150,24 @@ func (t *Transport) selectRetransmitsLocked(now time.Time) ([]retransmitRecord,
1133 return resend, emitDiagnostic1150 return resend, emitDiagnostic
1134 }1151 }
1135 1152
1136-// resendPending remains a synchronous test hook for retransmission selection1153+// resendPending remains a test hook for retransmission selection. Constructed
1137-// unit tests. Production timer paths use queueRetransmits and the sole sender.1154+// transports transfer its batch to the sole data sender; small unit fixtures
1155+// without transport goroutines execute the selected writes inline.
1138 func (t *Transport) resendPending() {1156 func (t *Transport) resendPending() {
1139 now := t.clock.Now()1157 now := t.clock.Now()
1140 t.mu.Lock()1158 t.mu.Lock()
1141 resend, _ := t.selectRetransmitsLocked(now)1159 resend, _ := t.selectRetransmitsLocked(now)
1142 t.mu.Unlock()1160 t.mu.Unlock()
1161+ if t.retransmitWork != nil {
1162+ select {
1163+ case t.retransmitWork <- resend:
1164+ case <-t.done:
1165+ }
1166+ return
1167+ }
1168+ pacer := bytePacer{clk: t.clock}
1143 for _, r := range resend {1169 for _, r := range resend {
1144- if err := t.sendData(r.seq, true, r.f); err == nil {1170+ if err := t.writeDataJob(&pacer, dataSendJob{seq: r.seq, reliable: true, frame: r.f}); err == nil {
1145 t.markPendingReady(r.seq, true)1171 t.markPendingReady(r.seq, true)
1146 } else {1172 } else {
1147 t.mu.Lock()1173 t.mu.Lock()
@@ -1194,6 +1220,7 @@ func (t *Transport) updateRTTLocked(sample time.Duration) {
1194 rto = t.maxResendAfter1220 rto = t.maxResendAfter
1195 }1221 }
1196 t.rto = rto1222 t.rto = rto
1223+ t.congestion.onRTT(t.srtt)
1197 }1224 }
1198 1225
1199 func (t *Transport) hopPacketConnOnce(generation uint64) {1226 func (t *Transport) hopPacketConnOnce(generation uint64) {
M internal/adapters/dgram/transport_test.go +168 -112
@@ -213,6 +213,148 @@ func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
213213 t.Fatalf("manual timers=%d, want at least %d", active, n)
214214 }
215215
216+func TestCongestionControllerBoundsAndIntegerGrowth(t *testing.T) {
217+ const mtu = 1200
218+ c := newCongestionController(mtu)
219+ if got, want := c.cwndBytes, initialCongestionPackets*mtu; got != want {
220+ t.Fatalf("initial cwnd=%d, want %d", got, want)
221+ }
222+ if got, want := c.burstBytes(), dataBurstPackets*mtu; got != want {
223+ t.Fatalf("initial burst=%d, want %d", got, want)
224+ }
225+ if got, want := c.bytesPerSecond(), initialCongestionPackets*mtu*int(time.Second/initialPacingRTT); got != want {
226+ t.Fatalf("initial pacing rate=%d, want %d", got, want)
227+ }
228+
229+ c.onLoss()
230+ if got, want := c.cwndBytes, initialCongestionPackets*mtu/2; got != want {
231+ t.Fatalf("cwnd after first loss=%d, want %d", got, want)
232+ }
233+ c.cwndBytes = 3 * mtu
234+ c.onLoss()
235+ if got, want := c.cwndBytes, minimumCongestionPackets*mtu; got != want {
236+ t.Fatalf("minimum cwnd=%d, want %d", got, want)
237+ }
238+
239+ c.cwndBytes = 10 * mtu
240+ c.onACK(1)
241+ if got, want := c.cwndBytes, 10*mtu+1; got != want {
242+ t.Fatalf("minimum additive increase cwnd=%d, want %d", got, want)
243+ }
244+ c.onACK(10 * mtu)
245+ if got, want := c.cwndBytes, 10*mtu+1+(mtu*10*mtu)/(10*mtu+1); got != want {
246+ t.Fatalf("integer additive increase cwnd=%d, want %d", got, want)
247+ }
248+ c.cwndBytes = maximumCongestionBytes
249+ c.onACK(maximumCongestionBytes)
250+ if got := c.cwndBytes; got != maximumCongestionBytes {
251+ t.Fatalf("maximum cwnd=%d, want %d", got, maximumCongestionBytes)
252+ }
253+
254+ c.onRTT(500 * time.Microsecond)
255+ if got, want := c.bytesPerSecond(), maximumCongestionBytes*1000; got != want {
256+ t.Fatalf("sub-millisecond pacing rate=%d, want %d", got, want)
257+ }
258+}
259+
260+func TestBytePacerUsesBurstThenFakeClockRate(t *testing.T) {
261+ const mtu = 1200
262+ clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
263+ c := newCongestionController(mtu)
264+ p := bytePacer{clk: clk}
265+ done := make(chan struct{})
266+ limits := func() (int, int) { return c.bytesPerSecond(), c.burstBytes() }
267+
268+ for range dataBurstPackets {
269+ if err := p.wait(done, mtu, limits); err != nil {
270+ t.Fatal(err)
271+ }
272+ }
273+ wait := make(chan error, 1)
274+ go func() { wait <- p.wait(done, mtu, limits) }()
275+ awaitSignal(t, clk.timerCreated, "byte pacer timer")
276+ clk.advance(24 * time.Millisecond)
277+ select {
278+ case err := <-wait:
279+ t.Fatalf("third MTU released early: %v", err)
280+ default:
281+ }
282+ clk.advance(time.Millisecond)
283+ if err := awaitResult(t, wait, "third paced MTU"); err != nil {
284+ t.Fatal(err)
285+ }
286+
287+ c.onLoss()
288+ next := make(chan error, 1)
289+ go func() { next <- p.wait(done, mtu, limits) }()
290+ awaitSignal(t, clk.timerCreated, "reduced-cwnd byte pacer timer")
291+ clk.advance(49 * time.Millisecond)
292+ select {
293+ case err := <-next:
294+ t.Fatalf("reduced cwnd did not lengthen wait: %v", err)
295+ default:
296+ }
297+ clk.advance(time.Millisecond)
298+ if err := awaitResult(t, next, "reduced-cwnd paced MTU"); err != nil {
299+ t.Fatal(err)
300+ }
301+}
302+
303+func TestBytePacerCancellation(t *testing.T) {
304+ const mtu = 1200
305+ clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
306+ p := bytePacer{clk: clk}
307+ done := make(chan struct{})
308+ limits := func() (int, int) { return mtu, mtu }
309+ if err := p.wait(done, mtu, limits); err != nil {
310+ t.Fatal(err)
311+ }
312+ wait := make(chan error, 1)
313+ go func() { wait <- p.wait(done, mtu, limits) }()
314+ awaitSignal(t, clk.timerCreated, "cancelled byte pacer timer")
315+ close(done)
316+ if err := awaitResult(t, wait, "byte pacer cancellation"); !errors.Is(err, errPacerClosed) {
317+ t.Fatalf("cancellation error=%v, want %v", err, errPacerClosed)
318+ }
319+}
320+
321+func BenchmarkCongestionControllerACK(b *testing.B) {
322+ c := newCongestionController(defaultMTU)
323+ b.ReportAllocs()
324+ for b.Loop() {
325+ c.onACK(defaultMTU)
326+ if c.cwndBytes == c.maxBytes {
327+ c = newCongestionController(defaultMTU)
328+ }
329+ }
330+}
331+
332+func TestSendAsyncTransfersFragmentedRecordToBytePacedSender(t *testing.T) {
333+ const mtu = 128
334+ aPC, bPC := newPair()
335+ clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
336+ a, err := NewTransportWithOptions(aPC, testAddr("b"), key(), 1, 2, Options{
337+ Clock: clk, MTU: mtu, ResendAfter: time.Hour, Heartbeat: time.Hour,
338+ })
339+ if err != nil {
340+ t.Fatal(err)
341+ }
342+ defer func() { _ = a.Close() }()
343+
344+ maxFragmentData := mtu - pdgram.HeaderSize - a.codec.Overhead()
345+ frame := ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 2*maxFragmentData)}
346+ if err := a.SendAsync(frame); err != nil {
347+ t.Fatal(err)
348+ }
349+ eventuallyPacketCount(t, bPC, dataBurstPackets)
350+ if got := len(bPC.in); got != dataBurstPackets {
351+ t.Fatalf("immediate datagrams=%d, want %d MTU burst", got, dataBurstPackets)
352+ }
353+ awaitSignal(t, clk.timerCreated, "shared data sender pacing timer")
354+ clk.advance(initialPacingRTT)
355+ eventuallyPacketCount(t, bPC, 3)
356+}
357+
216358 type deadlineCapturePC struct {
217359 addr net.Addr
218360 deadline atomic.Value
@@ -242,7 +384,7 @@ func (p *deadlineCapturePC) SetWriteDeadline(t time.Time) error {
242384 return nil
243385 }
244386
245-func TestOutputFirstSendIsPacedAndQueuedOutputsAreBatched(t *testing.T) {
387+func TestSmallOutputsShareInitialByteBurst(t *testing.T) {
246388 aPC, bPC := newPair()
247389 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
248390 a, err := NewTransportWithOptions(aPC, testAddr("b"), key(), 1, 2, Options{Clock: clk, ResendAfter: time.Hour, Heartbeat: time.Hour})
@@ -256,19 +398,10 @@ func TestOutputFirstSendIsPacedAndQueuedOutputsAreBatched(t *testing.T) {
256398 t.Fatal(err)
257399 }
258400 }
259- if got := len(bPC.in); got != 1 {
260- t.Fatalf("immediate datagrams=%d, want only the first output sent before pacing timer", got)
261- }
262- waitForManualTimers(t, clk, 4)
263- clk.advance(defaultOutputPaceMinDelay)
264401 eventuallyPacketCount(t, bPC, 3)
265402 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("next")}); err != nil {
266403 t.Fatal(err)
267404 }
268- if got := len(bPC.in); got != 3 {
269- t.Fatalf("datagrams before second pace tick=%d, want 3", got)
270- }
271- clk.advance(defaultOutputPaceMinDelay)
272405 eventuallyPacketCount(t, bPC, 4)
273406 }
274407
@@ -293,11 +426,11 @@ func TestOutputPacingSendsAtMostOneOversizedFramePerTick(t *testing.T) {
293426 waitForManualTimers(t, clk, 4)
294427 previousPackets := len(aPC.in)
295428 for tick := 0; tick < 128; tick++ {
296- clk.advance(defaultOutputPaceMaxDelay)
429+ clk.advance(initialPacingRTT)
297430 time.Sleep(time.Millisecond)
298431 packets := len(aPC.in)
299- if delta := packets - previousPackets; delta > defaultPacketPaceBudget {
300- t.Fatalf("tick %d emitted %d packets, budget %d", tick, delta, defaultPacketPaceBudget)
432+ if delta := packets - previousPackets; delta > initialCongestionPackets {
433+ t.Fatalf("tick %d emitted %d packets, cwnd packets %d", tick, delta, initialCongestionPackets)
301434 }
302435 previousPackets = packets
303436 a.mu.Lock()
@@ -323,7 +456,7 @@ func TestPacedInitialSendTimestampsFinalFragmentCompletion(t *testing.T) {
323456 defer func() { _ = a.Close() }()
324457 large := ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 24*a.mtu)}
325458 done := make(chan error, 1)
326- go func() { done <- a.SendAsync(large) }()
459+ go func() { done <- a.Send(large) }()
327460 start := clk.Now()
328461 for tick := 0; tick < 128; tick++ {
329462 a.mu.Lock()
@@ -333,7 +466,7 @@ func TestPacedInitialSendTimestampsFinalFragmentCompletion(t *testing.T) {
333466 t.Fatal("pending last timestamp set before final fragment")
334467 }
335468 a.mu.Unlock()
336- clk.advance(defaultOutputPaceMaxDelay)
469+ clk.advance(initialPacingRTT)
337470 time.Sleep(time.Millisecond)
338471 select {
339472 case err := <-done:
@@ -343,7 +476,7 @@ func TestPacedInitialSendTimestampsFinalFragmentCompletion(t *testing.T) {
343476 a.mu.Lock()
344477 completed := a.pending[1].last
345478 a.mu.Unlock()
346- if elapsed := completed.Sub(start); elapsed <= defaultOutputPaceMaxDelay {
479+ if elapsed := completed.Sub(start); elapsed <= initialPacingRTT {
347480 t.Fatalf("large send completed too early after %v", elapsed)
348481 }
349482 before := len(aPC.in)
@@ -396,7 +529,7 @@ func TestOwnedSynchronousMaxSideEffectCompletesThroughConcurrentPacedWork(t *tes
396529 go func() { done <- a.SendSynchronous(sideEffect) }()
397530
398531 for tick := 0; tick < 512; tick++ {
399- clk.advance(defaultOutputPaceMaxDelay)
532+ clk.advance(initialPacingRTT)
400533 time.Sleep(time.Millisecond)
401534 select {
402535 case err := <-done:
@@ -448,31 +581,6 @@ func TestResendSkipsOutputDuringFirstWireAttempt(t *testing.T) {
448581 }
449582 }
450583
451-func TestOutputPaceDelayUsesSRTTHalfClamped(t *testing.T) {
452- tr, _ := newResendTestTransport(t, 10)
453- tests := []struct {
454- name string
455- srtt time.Duration
456- want time.Duration
457- }{
458- {name: "high rtt clamps to max", srtt: time.Second, want: defaultOutputPaceMaxDelay},
459- {name: "low rtt clamps to min cadence", srtt: 30 * time.Millisecond, want: defaultOutputPaceMinCadence},
460- {name: "mid rtt uses half", srtt: 80 * time.Millisecond, want: 40 * time.Millisecond},
461- {name: "initial rtt uses min delay", want: defaultOutputPaceMinDelay},
462- }
463- for _, tt := range tests {
464- t.Run(tt.name, func(t *testing.T) {
465- tr.mu.Lock()
466- tr.srtt = tt.srtt
467- got := tr.outputPaceDelayLocked()
468- tr.mu.Unlock()
469- if got != tt.want {
470- t.Fatalf("output pace delay=%v, want %v", got, tt.want)
471- }
472- })
473- }
474-}
475-
476584 func TestInputWaitsForDequeuedOutputBatch(t *testing.T) {
477585 aPC, bPC := newPair()
478586 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
@@ -482,29 +590,12 @@ func TestInputWaitsForDequeuedOutputBatch(t *testing.T) {
482590 }
483591 defer func() { _ = a.Close() }()
484592
593+ a.writeMu.Lock()
485594 for _, payload := range []string{"first", "queued"} {
486595 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte(payload)}); err != nil {
487596 t.Fatal(err)
488597 }
489598 }
490- waitForManualTimers(t, clk, 4)
491- a.writeMu.Lock()
492- clk.advance(defaultOutputPaceMinDelay)
493- deadline := time.Now().Add(time.Second)
494- for {
495- a.mu.Lock()
496- dequeued := len(a.outputQueue) == 0
497- a.mu.Unlock()
498- if dequeued {
499- break
500- }
501- if time.Now().After(deadline) {
502- a.writeMu.Unlock()
503- t.Fatal("pacer did not dequeue output batch")
504- }
505- time.Sleep(time.Millisecond)
506- }
507-
508599 done := make(chan error, 1)
509600 go func() { done <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()
510601 select {
@@ -536,29 +627,13 @@ func TestPacedControlQueueFailureClosesTransport(t *testing.T) {
536627 }
537628 defer func() { _ = a.Close() }()
538629
539- for i := range 3 {
540- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte{byte(i)}}); err != nil {
541- t.Fatal(err)
542- }
543- }
544- if got := len(bPC.in); got != 1 {
545- t.Fatalf("immediate output datagrams=%d, want 1", got)
546- }
547630 if err := bPC.Close(); err != nil {
548631 t.Fatal(err)
549632 }
550-
551633 inputDone := make(chan error, 1)
552634 go func() { inputDone <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()
553- eventually(t, time.Second, func() bool {
554- a.mu.Lock()
555- defer a.mu.Unlock()
556- return len(a.outputQueue) == 2
557- })
558- waitForManualTimers(t, clk, 4)
559- clk.advance(defaultOutputPaceMinDelay)
560635 if err := <-inputDone; err == nil {
561- t.Fatal("queued control send succeeded after paced wire failure")
636+ t.Fatal("data send succeeded after paced wire failure")
562637 }
563638 select {
564639 case <-a.done:
@@ -602,10 +677,7 @@ func TestCloseUnblocksQueuedSynchronousFrames(t *testing.T) {
602677 if err != nil {
603678 t.Fatal(err)
604679 }
605- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("prime")}); err != nil {
606- t.Fatal(err)
607- }
608- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued")}); err != nil {
680+ if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 3*a.mtu)}); err != nil {
609681 t.Fatal(err)
610682 }
611683 done := make(chan error, 1)
@@ -613,7 +685,7 @@ func TestCloseUnblocksQueuedSynchronousFrames(t *testing.T) {
613685 eventually(t, time.Second, func() bool {
614686 a.mu.Lock()
615687 defer a.mu.Unlock()
616- return len(a.outputQueue) == 2
688+ return len(a.pending) == 2
617689 })
618690 _ = a.Close()
619691 if err := <-done; err == nil {
@@ -632,16 +704,12 @@ func TestOutputPaceFailureClosesTransportWithWriteError(t *testing.T) {
632704 }
633705 defer func() { _ = a.Close() }()
634706
635- for i := range 2 {
636- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte{byte(i)}}); err != nil {
637- t.Fatal(err)
638- }
639- }
640- waitForManualTimers(t, clk, 4)
641707 if err := bPC.Close(); err != nil {
642708 t.Fatal(err)
643709 }
644- clk.advance(defaultOutputPaceMinDelay)
710+ if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("fails")}); err != nil {
711+ t.Fatal(err)
712+ }
645713
646714 select {
647715 case <-a.done:
@@ -668,16 +736,9 @@ func TestInputAndControlRemainOrderedInBoundedPacedQueue(t *testing.T) {
668736 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued")}); err != nil {
669737 t.Fatal(err)
670738 }
671- if got := len(bPC.in); got != 1 {
672- t.Fatalf("immediate output datagrams=%d, want 1", got)
673- }
739+ eventuallyPacketCount(t, bPC, 2)
674740 inputDone := make(chan error, 1)
675741 go func() { inputDone <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()
676- if got := len(bPC.in); got != 1 {
677- t.Fatalf("datagrams after input=%d, want prerequisites and input still paced", got)
678- }
679- waitForManualTimers(t, clk, 4)
680- clk.advance(defaultOutputPaceMinDelay)
681742 eventuallyPacketCount(t, bPC, 3)
682743 if err := <-inputDone; err != nil {
683744 t.Fatal(err)
@@ -685,20 +746,8 @@ func TestInputAndControlRemainOrderedInBoundedPacedQueue(t *testing.T) {
685746 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued-before-control")}); err != nil {
686747 t.Fatal(err)
687748 }
688- if got := len(bPC.in); got != 3 {
689- t.Fatalf("datagrams before control=%d, want queued output still paced", got)
690- }
691749 controlDone := make(chan error, 1)
692750 go func() { controlDone <- a.Send(ports.Frame{Type: ports.MsgPing}) }()
693- eventually(t, time.Second, func() bool {
694- a.mu.Lock()
695- defer a.mu.Unlock()
696- return len(a.outputQueue) == 2
697- })
698- if got := len(bPC.in); got != 3 {
699- t.Fatalf("datagrams after control=%d, want ordered paced queue", got)
700- }
701- clk.advance(defaultOutputPaceMinDelay)
702751 eventuallyPacketCount(t, bPC, 5)
703752 if err := <-controlDone; err != nil {
704753 t.Fatal(err)
@@ -1179,6 +1228,11 @@ func TestImmediateLoopbackAckSamplesStampedFinalWrite(t *testing.T) {
11791228 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("state")}); err != nil {
11801229 t.Fatal(err)
11811230 }
1231+ eventually(t, time.Second, func() bool {
1232+ a.mu.Lock()
1233+ defer a.mu.Unlock()
1234+ return a.srtt > 0
1235+ })
11821236 a.mu.Lock()
11831237 srtt, rto := a.srtt, a.rto
11841238 a.mu.Unlock()
@@ -1293,6 +1347,8 @@ func newResendTestTransport(t *testing.T, maxResendPerTick int) (*Transport, *at
12931347 maxResendPerTick: maxResendPerTick,
12941348 rto: 100 * time.Millisecond,
12951349 clock: realClock{},
1350+ congestion: newCongestionController(pdgram.DefaultMTU),
1351+ done: make(chan struct{}),
12961352 }, &writes
12971353 }
12981354
@@ -1326,11 +1382,11 @@ func TestOversizedRetransmitHonorsPacketBudget(t *testing.T) {
13261382
13271383 previous := int32(0)
13281384 for tick := 0; tick < 128; tick++ {
1329- clk.advance(defaultOutputPaceMaxDelay)
1385+ clk.advance(initialPacingRTT)
13301386 time.Sleep(time.Millisecond)
13311387 current := writes.Load()
1332- if delta := current - previous; delta > defaultPacketPaceBudget {
1333- t.Fatalf("tick %d retransmitted %d packets, budget %d", tick, delta, defaultPacketPaceBudget)
1388+ if delta := current - previous; delta > dataBurstPackets {
1389+ t.Fatalf("tick %d retransmitted %d packets, burst %d", tick, delta, dataBurstPackets)
13341390 }
13351391 previous = current
13361392 select {
@@ -213,6 +213,148 @@ func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
213 t.Fatalf("manual timers=%d, want at least %d", active, n)213 t.Fatalf("manual timers=%d, want at least %d", active, n)
214 }214 }
215 215
216+func TestCongestionControllerBoundsAndIntegerGrowth(t *testing.T) {
217+ const mtu = 1200
218+ c := newCongestionController(mtu)
219+ if got, want := c.cwndBytes, initialCongestionPackets*mtu; got != want {
220+ t.Fatalf("initial cwnd=%d, want %d", got, want)
221+ }
222+ if got, want := c.burstBytes(), dataBurstPackets*mtu; got != want {
223+ t.Fatalf("initial burst=%d, want %d", got, want)
224+ }
225+ if got, want := c.bytesPerSecond(), initialCongestionPackets*mtu*int(time.Second/initialPacingRTT); got != want {
226+ t.Fatalf("initial pacing rate=%d, want %d", got, want)
227+ }
228+
229+ c.onLoss()
230+ if got, want := c.cwndBytes, initialCongestionPackets*mtu/2; got != want {
231+ t.Fatalf("cwnd after first loss=%d, want %d", got, want)
232+ }
233+ c.cwndBytes = 3 * mtu
234+ c.onLoss()
235+ if got, want := c.cwndBytes, minimumCongestionPackets*mtu; got != want {
236+ t.Fatalf("minimum cwnd=%d, want %d", got, want)
237+ }
238+
239+ c.cwndBytes = 10 * mtu
240+ c.onACK(1)
241+ if got, want := c.cwndBytes, 10*mtu+1; got != want {
242+ t.Fatalf("minimum additive increase cwnd=%d, want %d", got, want)
243+ }
244+ c.onACK(10 * mtu)
245+ if got, want := c.cwndBytes, 10*mtu+1+(mtu*10*mtu)/(10*mtu+1); got != want {
246+ t.Fatalf("integer additive increase cwnd=%d, want %d", got, want)
247+ }
248+ c.cwndBytes = maximumCongestionBytes
249+ c.onACK(maximumCongestionBytes)
250+ if got := c.cwndBytes; got != maximumCongestionBytes {
251+ t.Fatalf("maximum cwnd=%d, want %d", got, maximumCongestionBytes)
252+ }
253+
254+ c.onRTT(500 * time.Microsecond)
255+ if got, want := c.bytesPerSecond(), maximumCongestionBytes*1000; got != want {
256+ t.Fatalf("sub-millisecond pacing rate=%d, want %d", got, want)
257+ }
258+}
259+
260+func TestBytePacerUsesBurstThenFakeClockRate(t *testing.T) {
261+ const mtu = 1200
262+ clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
263+ c := newCongestionController(mtu)
264+ p := bytePacer{clk: clk}
265+ done := make(chan struct{})
266+ limits := func() (int, int) { return c.bytesPerSecond(), c.burstBytes() }
267+
268+ for range dataBurstPackets {
269+ if err := p.wait(done, mtu, limits); err != nil {
270+ t.Fatal(err)
271+ }
272+ }
273+ wait := make(chan error, 1)
274+ go func() { wait <- p.wait(done, mtu, limits) }()
275+ awaitSignal(t, clk.timerCreated, "byte pacer timer")
276+ clk.advance(24 * time.Millisecond)
277+ select {
278+ case err := <-wait:
279+ t.Fatalf("third MTU released early: %v", err)
280+ default:
281+ }
282+ clk.advance(time.Millisecond)
283+ if err := awaitResult(t, wait, "third paced MTU"); err != nil {
284+ t.Fatal(err)
285+ }
286+
287+ c.onLoss()
288+ next := make(chan error, 1)
289+ go func() { next <- p.wait(done, mtu, limits) }()
290+ awaitSignal(t, clk.timerCreated, "reduced-cwnd byte pacer timer")
291+ clk.advance(49 * time.Millisecond)
292+ select {
293+ case err := <-next:
294+ t.Fatalf("reduced cwnd did not lengthen wait: %v", err)
295+ default:
296+ }
297+ clk.advance(time.Millisecond)
298+ if err := awaitResult(t, next, "reduced-cwnd paced MTU"); err != nil {
299+ t.Fatal(err)
300+ }
301+}
302+
303+func TestBytePacerCancellation(t *testing.T) {
304+ const mtu = 1200
305+ clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
306+ p := bytePacer{clk: clk}
307+ done := make(chan struct{})
308+ limits := func() (int, int) { return mtu, mtu }
309+ if err := p.wait(done, mtu, limits); err != nil {
310+ t.Fatal(err)
311+ }
312+ wait := make(chan error, 1)
313+ go func() { wait <- p.wait(done, mtu, limits) }()
314+ awaitSignal(t, clk.timerCreated, "cancelled byte pacer timer")
315+ close(done)
316+ if err := awaitResult(t, wait, "byte pacer cancellation"); !errors.Is(err, errPacerClosed) {
317+ t.Fatalf("cancellation error=%v, want %v", err, errPacerClosed)
318+ }
319+}
320+
321+func BenchmarkCongestionControllerACK(b *testing.B) {
322+ c := newCongestionController(defaultMTU)
323+ b.ReportAllocs()
324+ for b.Loop() {
325+ c.onACK(defaultMTU)
326+ if c.cwndBytes == c.maxBytes {
327+ c = newCongestionController(defaultMTU)
328+ }
329+ }
330+}
331+
332+func TestSendAsyncTransfersFragmentedRecordToBytePacedSender(t *testing.T) {
333+ const mtu = 128
334+ aPC, bPC := newPair()
335+ clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
336+ a, err := NewTransportWithOptions(aPC, testAddr("b"), key(), 1, 2, Options{
337+ Clock: clk, MTU: mtu, ResendAfter: time.Hour, Heartbeat: time.Hour,
338+ })
339+ if err != nil {
340+ t.Fatal(err)
341+ }
342+ defer func() { _ = a.Close() }()
343+
344+ maxFragmentData := mtu - pdgram.HeaderSize - a.codec.Overhead()
345+ frame := ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 2*maxFragmentData)}
346+ if err := a.SendAsync(frame); err != nil {
347+ t.Fatal(err)
348+ }
349+ eventuallyPacketCount(t, bPC, dataBurstPackets)
350+ if got := len(bPC.in); got != dataBurstPackets {
351+ t.Fatalf("immediate datagrams=%d, want %d MTU burst", got, dataBurstPackets)
352+ }
353+ awaitSignal(t, clk.timerCreated, "shared data sender pacing timer")
354+ clk.advance(initialPacingRTT)
355+ eventuallyPacketCount(t, bPC, 3)
356+}
357+
216 type deadlineCapturePC struct {358 type deadlineCapturePC struct {
217 addr net.Addr359 addr net.Addr
218 deadline atomic.Value360 deadline atomic.Value
@@ -242,7 +384,7 @@ func (p *deadlineCapturePC) SetWriteDeadline(t time.Time) error {
242 return nil384 return nil
243 }385 }
244 386
245-func TestOutputFirstSendIsPacedAndQueuedOutputsAreBatched(t *testing.T) {387+func TestSmallOutputsShareInitialByteBurst(t *testing.T) {
246 aPC, bPC := newPair()388 aPC, bPC := newPair()
247 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))389 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
248 a, err := NewTransportWithOptions(aPC, testAddr("b"), key(), 1, 2, Options{Clock: clk, ResendAfter: time.Hour, Heartbeat: time.Hour})390 a, err := NewTransportWithOptions(aPC, testAddr("b"), key(), 1, 2, Options{Clock: clk, ResendAfter: time.Hour, Heartbeat: time.Hour})
@@ -256,19 +398,10 @@ func TestOutputFirstSendIsPacedAndQueuedOutputsAreBatched(t *testing.T) {
256 t.Fatal(err)398 t.Fatal(err)
257 }399 }
258 }400 }
259- if got := len(bPC.in); got != 1 {
260- t.Fatalf("immediate datagrams=%d, want only the first output sent before pacing timer", got)
261- }
262- waitForManualTimers(t, clk, 4)
263- clk.advance(defaultOutputPaceMinDelay)
264 eventuallyPacketCount(t, bPC, 3)401 eventuallyPacketCount(t, bPC, 3)
265 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("next")}); err != nil {402 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("next")}); err != nil {
266 t.Fatal(err)403 t.Fatal(err)
267 }404 }
268- if got := len(bPC.in); got != 3 {
269- t.Fatalf("datagrams before second pace tick=%d, want 3", got)
270- }
271- clk.advance(defaultOutputPaceMinDelay)
272 eventuallyPacketCount(t, bPC, 4)405 eventuallyPacketCount(t, bPC, 4)
273 }406 }
274 407
@@ -293,11 +426,11 @@ func TestOutputPacingSendsAtMostOneOversizedFramePerTick(t *testing.T) {
293 waitForManualTimers(t, clk, 4)426 waitForManualTimers(t, clk, 4)
294 previousPackets := len(aPC.in)427 previousPackets := len(aPC.in)
295 for tick := 0; tick < 128; tick++ {428 for tick := 0; tick < 128; tick++ {
296- clk.advance(defaultOutputPaceMaxDelay)429+ clk.advance(initialPacingRTT)
297 time.Sleep(time.Millisecond)430 time.Sleep(time.Millisecond)
298 packets := len(aPC.in)431 packets := len(aPC.in)
299- if delta := packets - previousPackets; delta > defaultPacketPaceBudget {432+ if delta := packets - previousPackets; delta > initialCongestionPackets {
300- t.Fatalf("tick %d emitted %d packets, budget %d", tick, delta, defaultPacketPaceBudget)433+ t.Fatalf("tick %d emitted %d packets, cwnd packets %d", tick, delta, initialCongestionPackets)
301 }434 }
302 previousPackets = packets435 previousPackets = packets
303 a.mu.Lock()436 a.mu.Lock()
@@ -323,7 +456,7 @@ func TestPacedInitialSendTimestampsFinalFragmentCompletion(t *testing.T) {
323 defer func() { _ = a.Close() }()456 defer func() { _ = a.Close() }()
324 large := ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 24*a.mtu)}457 large := ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 24*a.mtu)}
325 done := make(chan error, 1)458 done := make(chan error, 1)
326- go func() { done <- a.SendAsync(large) }()459+ go func() { done <- a.Send(large) }()
327 start := clk.Now()460 start := clk.Now()
328 for tick := 0; tick < 128; tick++ {461 for tick := 0; tick < 128; tick++ {
329 a.mu.Lock()462 a.mu.Lock()
@@ -333,7 +466,7 @@ func TestPacedInitialSendTimestampsFinalFragmentCompletion(t *testing.T) {
333 t.Fatal("pending last timestamp set before final fragment")466 t.Fatal("pending last timestamp set before final fragment")
334 }467 }
335 a.mu.Unlock()468 a.mu.Unlock()
336- clk.advance(defaultOutputPaceMaxDelay)469+ clk.advance(initialPacingRTT)
337 time.Sleep(time.Millisecond)470 time.Sleep(time.Millisecond)
338 select {471 select {
339 case err := <-done:472 case err := <-done:
@@ -343,7 +476,7 @@ func TestPacedInitialSendTimestampsFinalFragmentCompletion(t *testing.T) {
343 a.mu.Lock()476 a.mu.Lock()
344 completed := a.pending[1].last477 completed := a.pending[1].last
345 a.mu.Unlock()478 a.mu.Unlock()
346- if elapsed := completed.Sub(start); elapsed <= defaultOutputPaceMaxDelay {479+ if elapsed := completed.Sub(start); elapsed <= initialPacingRTT {
347 t.Fatalf("large send completed too early after %v", elapsed)480 t.Fatalf("large send completed too early after %v", elapsed)
348 }481 }
349 before := len(aPC.in)482 before := len(aPC.in)
@@ -396,7 +529,7 @@ func TestOwnedSynchronousMaxSideEffectCompletesThroughConcurrentPacedWork(t *tes
396 go func() { done <- a.SendSynchronous(sideEffect) }()529 go func() { done <- a.SendSynchronous(sideEffect) }()
397 530
398 for tick := 0; tick < 512; tick++ {531 for tick := 0; tick < 512; tick++ {
399- clk.advance(defaultOutputPaceMaxDelay)532+ clk.advance(initialPacingRTT)
400 time.Sleep(time.Millisecond)533 time.Sleep(time.Millisecond)
401 select {534 select {
402 case err := <-done:535 case err := <-done:
@@ -448,31 +581,6 @@ func TestResendSkipsOutputDuringFirstWireAttempt(t *testing.T) {
448 }581 }
449 }582 }
450 583
451-func TestOutputPaceDelayUsesSRTTHalfClamped(t *testing.T) {
452- tr, _ := newResendTestTransport(t, 10)
453- tests := []struct {
454- name string
455- srtt time.Duration
456- want time.Duration
457- }{
458- {name: "high rtt clamps to max", srtt: time.Second, want: defaultOutputPaceMaxDelay},
459- {name: "low rtt clamps to min cadence", srtt: 30 * time.Millisecond, want: defaultOutputPaceMinCadence},
460- {name: "mid rtt uses half", srtt: 80 * time.Millisecond, want: 40 * time.Millisecond},
461- {name: "initial rtt uses min delay", want: defaultOutputPaceMinDelay},
462- }
463- for _, tt := range tests {
464- t.Run(tt.name, func(t *testing.T) {
465- tr.mu.Lock()
466- tr.srtt = tt.srtt
467- got := tr.outputPaceDelayLocked()
468- tr.mu.Unlock()
469- if got != tt.want {
470- t.Fatalf("output pace delay=%v, want %v", got, tt.want)
471- }
472- })
473- }
474-}
475-
476 func TestInputWaitsForDequeuedOutputBatch(t *testing.T) {584 func TestInputWaitsForDequeuedOutputBatch(t *testing.T) {
477 aPC, bPC := newPair()585 aPC, bPC := newPair()
478 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))586 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
@@ -482,29 +590,12 @@ func TestInputWaitsForDequeuedOutputBatch(t *testing.T) {
482 }590 }
483 defer func() { _ = a.Close() }()591 defer func() { _ = a.Close() }()
484 592
593+ a.writeMu.Lock()
485 for _, payload := range []string{"first", "queued"} {594 for _, payload := range []string{"first", "queued"} {
486 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte(payload)}); err != nil {595 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte(payload)}); err != nil {
487 t.Fatal(err)596 t.Fatal(err)
488 }597 }
489 }598 }
490- waitForManualTimers(t, clk, 4)
491- a.writeMu.Lock()
492- clk.advance(defaultOutputPaceMinDelay)
493- deadline := time.Now().Add(time.Second)
494- for {
495- a.mu.Lock()
496- dequeued := len(a.outputQueue) == 0
497- a.mu.Unlock()
498- if dequeued {
499- break
500- }
501- if time.Now().After(deadline) {
502- a.writeMu.Unlock()
503- t.Fatal("pacer did not dequeue output batch")
504- }
505- time.Sleep(time.Millisecond)
506- }
507-
508 done := make(chan error, 1)599 done := make(chan error, 1)
509 go func() { done <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()600 go func() { done <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()
510 select {601 select {
@@ -536,29 +627,13 @@ func TestPacedControlQueueFailureClosesTransport(t *testing.T) {
536 }627 }
537 defer func() { _ = a.Close() }()628 defer func() { _ = a.Close() }()
538 629
539- for i := range 3 {
540- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte{byte(i)}}); err != nil {
541- t.Fatal(err)
542- }
543- }
544- if got := len(bPC.in); got != 1 {
545- t.Fatalf("immediate output datagrams=%d, want 1", got)
546- }
547 if err := bPC.Close(); err != nil {630 if err := bPC.Close(); err != nil {
548 t.Fatal(err)631 t.Fatal(err)
549 }632 }
550-
551 inputDone := make(chan error, 1)633 inputDone := make(chan error, 1)
552 go func() { inputDone <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()634 go func() { inputDone <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()
553- eventually(t, time.Second, func() bool {
554- a.mu.Lock()
555- defer a.mu.Unlock()
556- return len(a.outputQueue) == 2
557- })
558- waitForManualTimers(t, clk, 4)
559- clk.advance(defaultOutputPaceMinDelay)
560 if err := <-inputDone; err == nil {635 if err := <-inputDone; err == nil {
561- t.Fatal("queued control send succeeded after paced wire failure")636+ t.Fatal("data send succeeded after paced wire failure")
562 }637 }
563 select {638 select {
564 case <-a.done:639 case <-a.done:
@@ -602,10 +677,7 @@ func TestCloseUnblocksQueuedSynchronousFrames(t *testing.T) {
602 if err != nil {677 if err != nil {
603 t.Fatal(err)678 t.Fatal(err)
604 }679 }
605- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("prime")}); err != nil {680+ if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 3*a.mtu)}); err != nil {
606- t.Fatal(err)
607- }
608- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued")}); err != nil {
609 t.Fatal(err)681 t.Fatal(err)
610 }682 }
611 done := make(chan error, 1)683 done := make(chan error, 1)
@@ -613,7 +685,7 @@ func TestCloseUnblocksQueuedSynchronousFrames(t *testing.T) {
613 eventually(t, time.Second, func() bool {685 eventually(t, time.Second, func() bool {
614 a.mu.Lock()686 a.mu.Lock()
615 defer a.mu.Unlock()687 defer a.mu.Unlock()
616- return len(a.outputQueue) == 2688+ return len(a.pending) == 2
617 })689 })
618 _ = a.Close()690 _ = a.Close()
619 if err := <-done; err == nil {691 if err := <-done; err == nil {
@@ -632,16 +704,12 @@ func TestOutputPaceFailureClosesTransportWithWriteError(t *testing.T) {
632 }704 }
633 defer func() { _ = a.Close() }()705 defer func() { _ = a.Close() }()
634 706
635- for i := range 2 {
636- if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte{byte(i)}}); err != nil {
637- t.Fatal(err)
638- }
639- }
640- waitForManualTimers(t, clk, 4)
641 if err := bPC.Close(); err != nil {707 if err := bPC.Close(); err != nil {
642 t.Fatal(err)708 t.Fatal(err)
643 }709 }
644- clk.advance(defaultOutputPaceMinDelay)710+ if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("fails")}); err != nil {
711+ t.Fatal(err)
712+ }
645 713
646 select {714 select {
647 case <-a.done:715 case <-a.done:
@@ -668,16 +736,9 @@ func TestInputAndControlRemainOrderedInBoundedPacedQueue(t *testing.T) {
668 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued")}); err != nil {736 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued")}); err != nil {
669 t.Fatal(err)737 t.Fatal(err)
670 }738 }
671- if got := len(bPC.in); got != 1 {739+ eventuallyPacketCount(t, bPC, 2)
672- t.Fatalf("immediate output datagrams=%d, want 1", got)
673- }
674 inputDone := make(chan error, 1)740 inputDone := make(chan error, 1)
675 go func() { inputDone <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()741 go func() { inputDone <- a.Send(ports.Frame{Type: ports.MsgInput, Payload: []byte("typed")}) }()
676- if got := len(bPC.in); got != 1 {
677- t.Fatalf("datagrams after input=%d, want prerequisites and input still paced", got)
678- }
679- waitForManualTimers(t, clk, 4)
680- clk.advance(defaultOutputPaceMinDelay)
681 eventuallyPacketCount(t, bPC, 3)742 eventuallyPacketCount(t, bPC, 3)
682 if err := <-inputDone; err != nil {743 if err := <-inputDone; err != nil {
683 t.Fatal(err)744 t.Fatal(err)
@@ -685,20 +746,8 @@ func TestInputAndControlRemainOrderedInBoundedPacedQueue(t *testing.T) {
685 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued-before-control")}); err != nil {746 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("queued-before-control")}); err != nil {
686 t.Fatal(err)747 t.Fatal(err)
687 }748 }
688- if got := len(bPC.in); got != 3 {
689- t.Fatalf("datagrams before control=%d, want queued output still paced", got)
690- }
691 controlDone := make(chan error, 1)749 controlDone := make(chan error, 1)
692 go func() { controlDone <- a.Send(ports.Frame{Type: ports.MsgPing}) }()750 go func() { controlDone <- a.Send(ports.Frame{Type: ports.MsgPing}) }()
693- eventually(t, time.Second, func() bool {
694- a.mu.Lock()
695- defer a.mu.Unlock()
696- return len(a.outputQueue) == 2
697- })
698- if got := len(bPC.in); got != 3 {
699- t.Fatalf("datagrams after control=%d, want ordered paced queue", got)
700- }
701- clk.advance(defaultOutputPaceMinDelay)
702 eventuallyPacketCount(t, bPC, 5)751 eventuallyPacketCount(t, bPC, 5)
703 if err := <-controlDone; err != nil {752 if err := <-controlDone; err != nil {
704 t.Fatal(err)753 t.Fatal(err)
@@ -1179,6 +1228,11 @@ func TestImmediateLoopbackAckSamplesStampedFinalWrite(t *testing.T) {
1179 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("state")}); err != nil {1228 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("state")}); err != nil {
1180 t.Fatal(err)1229 t.Fatal(err)
1181 }1230 }
1231+ eventually(t, time.Second, func() bool {
1232+ a.mu.Lock()
1233+ defer a.mu.Unlock()
1234+ return a.srtt > 0
1235+ })
1182 a.mu.Lock()1236 a.mu.Lock()
1183 srtt, rto := a.srtt, a.rto1237 srtt, rto := a.srtt, a.rto
1184 a.mu.Unlock()1238 a.mu.Unlock()
@@ -1293,6 +1347,8 @@ func newResendTestTransport(t *testing.T, maxResendPerTick int) (*Transport, *at
1293 maxResendPerTick: maxResendPerTick,1347 maxResendPerTick: maxResendPerTick,
1294 rto: 100 * time.Millisecond,1348 rto: 100 * time.Millisecond,
1295 clock: realClock{},1349 clock: realClock{},
1350+ congestion: newCongestionController(pdgram.DefaultMTU),
1351+ done: make(chan struct{}),
1296 }, &writes1352 }, &writes
1297 }1353 }
1298 1354
@@ -1326,11 +1382,11 @@ func TestOversizedRetransmitHonorsPacketBudget(t *testing.T) {
1326 1382
1327 previous := int32(0)1383 previous := int32(0)
1328 for tick := 0; tick < 128; tick++ {1384 for tick := 0; tick < 128; tick++ {
1329- clk.advance(defaultOutputPaceMaxDelay)1385+ clk.advance(initialPacingRTT)
1330 time.Sleep(time.Millisecond)1386 time.Sleep(time.Millisecond)
1331 current := writes.Load()1387 current := writes.Load()
1332- if delta := current - previous; delta > defaultPacketPaceBudget {1388+ if delta := current - previous; delta > dataBurstPackets {
1333- t.Fatalf("tick %d retransmitted %d packets, budget %d", tick, delta, defaultPacketPaceBudget)1389+ t.Fatalf("tick %d retransmitted %d packets, burst %d", tick, delta, dataBurstPackets)
1334 }1390 }
1335 previous = current1391 previous = current
1336 select {1392 select {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close