detached d4ed0f5

fix: address UDP transport review findings

commit
d4ed0f52a2ef5dcb899eb751869a430752f9f6a0
author
brice · · 2026-07-10 11:52 UTC
parent
7943a2a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +180 -27
M internal/adapters/dgram/control.go +18 -7
@@ -40,6 +40,12 @@ func (t *Transport) queueACK(seq uint64) {
4040 case t.ackWake <- struct{}{}:
4141 default:
4242 }
43+ t.mu.Lock()
44+ afterACKQueued := t.afterACKQueued
45+ t.mu.Unlock()
46+ if afterACKQueued != nil {
47+ afterACKQueued()
48+ }
4349 }
4450
4551 // queueControl is deliberately lossy when the bounded control queue is full:
@@ -93,15 +99,20 @@ func (t *Transport) ackScheduleLoop() {
9399 }
94100 }()
95101 scheduleACK := func() {
96- if ackTimerC != nil {
97- return
102+ if ackTimerC == nil {
103+ if ackTimer == nil {
104+ ackTimer = t.clock.NewTimer(maxACKDelay)
105+ } else {
106+ ackTimer.Reset(maxACKDelay)
107+ }
108+ ackTimerC = ackTimer.C()
98109 }
99- if ackTimer == nil {
100- ackTimer = t.clock.NewTimer(maxACKDelay)
101- } else {
102- ackTimer.Reset(maxACKDelay)
110+ t.mu.Lock()
111+ afterACKScheduled := t.afterACKScheduled
112+ t.mu.Unlock()
113+ if afterACKScheduled != nil {
114+ afterACKScheduled()
103115 }
104- ackTimerC = ackTimer.C()
105116 }
106117 dispatchACK := func() {
107118 seq, ok := t.takeACK()
@@ -40,6 +40,12 @@ func (t *Transport) queueACK(seq uint64) {
40 case t.ackWake <- struct{}{}:40 case t.ackWake <- struct{}{}:
41 default:41 default:
42 }42 }
43+ t.mu.Lock()
44+ afterACKQueued := t.afterACKQueued
45+ t.mu.Unlock()
46+ if afterACKQueued != nil {
47+ afterACKQueued()
48+ }
43 }49 }
44 50
45 // queueControl is deliberately lossy when the bounded control queue is full:51 // queueControl is deliberately lossy when the bounded control queue is full:
@@ -93,15 +99,20 @@ func (t *Transport) ackScheduleLoop() {
93 }99 }
94 }()100 }()
95 scheduleACK := func() {101 scheduleACK := func() {
96- if ackTimerC != nil {102+ if ackTimerC == nil {
97- return103+ if ackTimer == nil {
104+ ackTimer = t.clock.NewTimer(maxACKDelay)
105+ } else {
106+ ackTimer.Reset(maxACKDelay)
107+ }
108+ ackTimerC = ackTimer.C()
98 }109 }
99- if ackTimer == nil {110+ t.mu.Lock()
100- ackTimer = t.clock.NewTimer(maxACKDelay)111+ afterACKScheduled := t.afterACKScheduled
101- } else {112+ t.mu.Unlock()
102- ackTimer.Reset(maxACKDelay)113+ if afterACKScheduled != nil {
114+ afterACKScheduled()
103 }115 }
104- ackTimerC = ackTimer.C()
105 }116 }
106 dispatchACK := func() {117 dispatchACK := func() {
107 seq, ok := t.takeACK()118 seq, ok := t.takeACK()
M internal/adapters/dgram/network_sim_test.go +123 -10
@@ -94,16 +94,20 @@ func (l *simulatedLink) enqueue(p *fakePC, peer *fakePC, b []byte) bool {
9494 return true
9595 }
9696
97-func (l *simulatedLink) flush(peer *fakePC) {
97+func (l *simulatedLink) flush(peer *fakePC) int {
9898 l.mu.Lock()
9999 defer l.mu.Unlock()
100100 now := l.clock.Now()
101+ delivered := 0
101102 for len(l.queue) > 0 && !l.queue[0].due.After(now) {
102103 q := l.queue[0]
103104 l.queue = l.queue[1:]
104105 l.queueBytes -= len(q.b)
105- l.deliverLocked(peer, q.packet)
106+ if l.deliverLocked(peer, q.packet) {
107+ delivered++
108+ }
106109 }
110+ return delivered
107111 }
108112
109113 func (l *simulatedLink) queuedBytes() int {
@@ -179,6 +183,7 @@ func (p *fakePC) ReadFrom(b []byte) (int, net.Addr, error) {
179183 func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
180184 p.mu.Lock()
181185 closed := p.closed
186+ afterWrite := p.afterWrite
182187 p.mu.Unlock()
183188 if closed {
184189 return 0, errors.New("closed")
@@ -204,11 +209,29 @@ func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
204209 }
205210 peer.mu.Unlock()
206211 }
207- if p.afterWrite != nil {
208- p.afterWrite()
212+ if afterWrite != nil {
213+ afterWrite()
209214 }
210215 return len(b), nil
211216 }
217+func (p *fakePC) replaceAfterWrite(afterWrite func()) {
218+ p.mu.Lock()
219+ defer p.mu.Unlock()
220+ p.afterWrite = afterWrite
221+}
222+
223+func (p *fakePC) addAfterWrite(afterWrite func()) {
224+ p.mu.Lock()
225+ defer p.mu.Unlock()
226+ previous := p.afterWrite
227+ p.afterWrite = func() {
228+ if previous != nil {
229+ previous()
230+ }
231+ afterWrite()
232+ }
233+}
234+
212235 func (p *fakePC) Close() error {
213236 p.mu.Lock()
214237 defer p.mu.Unlock()
@@ -255,6 +278,15 @@ func awaitResult[T any](t *testing.T, ch <-chan T, context string) T {
255278 }
256279 }
257280
281+func flushAndAwait(t *testing.T, link *simulatedLink, peer *fakePC, processed <-chan struct{}) int {
282+ t.Helper()
283+ delivered := link.flush(peer)
284+ for range delivered {
285+ awaitSignal(t, processed, "flushed packet processing")
286+ }
287+ return delivered
288+}
289+
258290 // TestTransportFloodClassification drives authenticated datagrams through real
259291 // transports. The metrics are transport state, not simulator bookkeeping.
260292 func TestTransportFloodClassification(t *testing.T) {
@@ -356,19 +388,65 @@ func TestTransportFloodClassification(t *testing.T) {
356388 defer closeFloodTransports(a, b)
357389 waitForManualTimers(t, clk, 6)
358390 var contacted atomic.Bool
391+ processed := make(chan struct{})
392+ primerWritten := make(chan struct{}, 1)
393+ primerACKScheduled := make(chan struct{}, 1)
394+ primerAcknowledged := make(chan struct{}, 1)
395+ ackQueued := make(chan struct{}, 128)
396+ ackScheduled := make(chan struct{}, 128)
397+ ackWritten := make(chan struct{}, 128)
398+ aPC.replaceAfterWrite(func() {
399+ select {
400+ case primerWritten <- struct{}{}:
401+ default:
402+ }
403+ })
404+ bPC.replaceAfterWrite(func() {
405+ select {
406+ case primerAcknowledged <- struct{}{}:
407+ default:
408+ }
409+ select {
410+ case ackWritten <- struct{}{}:
411+ default:
412+ }
413+ })
414+ a.mu.Lock()
415+ a.afterPacketProcessed = func() { processed <- struct{}{} }
416+ a.mu.Unlock()
359417 b.mu.Lock()
360418 b.afterAuthenticatedPacket = func() { contacted.Store(true) }
419+ b.afterPacketProcessed = func() { processed <- struct{}{} }
420+ b.afterACKScheduled = func() {
421+ select {
422+ case primerACKScheduled <- struct{}{}:
423+ default:
424+ }
425+ select {
426+ case ackScheduled <- struct{}{}:
427+ default:
428+ }
429+ }
430+ b.afterACKQueued = func() { ackQueued <- struct{}{} }
361431 b.mu.Unlock()
362432 primer := ports.MarshalOutput(ports.Output{BaseStateNum: 0, NewStateNum: 1, Data: []byte("primer")})
363433 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: primer}); err != nil {
364434 t.Fatal(err)
365435 }
436+ awaitSignal(t, primerWritten, "primer write")
366437 primerDrained := false
438+ awaitingPrimerACK := false
367439 for range 40 {
368440 clk.advance(25 * time.Millisecond)
369- forward.flush(bPC)
370- reverse.flush(aPC)
371- time.Sleep(100 * time.Microsecond)
441+ if awaitingPrimerACK {
442+ awaitSignal(t, primerAcknowledged, "primer ACK write")
443+ awaitingPrimerACK = false
444+ }
445+ if flushAndAwait(t, forward, bPC, processed) > 0 {
446+ awaitSignal(t, primerACKScheduled, "primer ACK scheduling")
447+ awaitingPrimerACK = true
448+ }
449+ flushAndAwait(t, reverse, aPC, processed)
372450 a.mu.Lock()
373451 primerDrained = len(a.pending) == 0
374452 a.mu.Unlock()
@@ -379,9 +457,22 @@ func TestTransportFloodClassification(t *testing.T) {
379457 if !primerDrained {
380458 t.Fatal("clean RTT primer did not drain")
381459 }
460+ <-ackQueued
461+ <-ackScheduled
382462 a.mu.Lock()
383463 initialCwnd := a.congestion.cwndBytes
384464 a.mu.Unlock()
465+ dataPaceStarted := make(chan struct{}, 128)
466+ dataWritten := make(chan struct{}, 1)
467+ a.mu.Lock()
468+ a.beforeDataPace = func() { dataPaceStarted <- struct{}{} }
469+ a.mu.Unlock()
470+ aPC.addAfterWrite(func() {
471+ select {
472+ case dataWritten <- struct{}{}:
473+ default:
474+ }
475+ })
385476 for state := 1; state < outputCount; state++ {
386477 payload := ports.MarshalOutput(ports.Output{
387478 BaseStateNum: uint64(state),
@@ -392,8 +483,11 @@ func TestTransportFloodClassification(t *testing.T) {
392483 t.Fatal(err)
393484 }
394485 }
486+ awaitSignal(t, dataPaceStarted, "output pacing")
487+ awaitSignal(t, dataWritten, "first paced output write")
395488
396489 var maxContactAge time.Duration
490+ pendingACKWrites := 0
397491 lossReducedCwnd := false
398492 latestState := uint64(0)
399493 probed := false
@@ -401,9 +495,28 @@ func TestTransportFloodClassification(t *testing.T) {
401495 probeWho := ""
402496 for range 2000 {
403497 clk.advance(25 * time.Millisecond)
404- forward.flush(bPC)
405- reverse.flush(aPC)
406- time.Sleep(100 * time.Microsecond)
498+ for range pendingACKWrites {
499+ awaitSignal(t, ackWritten, "cumulative ACK write")
500+ }
501+ pendingACKWrites = 0
502+ flushAndAwait(t, forward, bPC, processed)
503+ for {
504+ select {
505+ case <-ackQueued:
506+ awaitSignal(t, ackScheduled, "cumulative ACK scheduling")
507+ pendingACKWrites++
508+ default:
509+ goto ackRecordsDrained
510+ }
511+ }
512+ ackRecordsDrained:
513+ flushAndAwait(t, reverse, aPC, processed)
514+ a.mu.Lock()
515+ queuedOutput := len(a.outputQueue) > 0
516+ a.mu.Unlock()
517+ if queuedOutput {
518+ awaitSignal(t, dataPaceStarted, "output pacing")
519+ }
407520
408521 if forward.queuedBytes() > queueLimit || reverse.queuedBytes() > queueLimit {
409522 t.Fatalf("queue exceeded bound: forward=%d reverse=%d limit=%d", forward.queuedBytes(), reverse.queuedBytes(), queueLimit)
@@ -94,16 +94,20 @@ func (l *simulatedLink) enqueue(p *fakePC, peer *fakePC, b []byte) bool {
94 return true94 return true
95 }95 }
96 96
97-func (l *simulatedLink) flush(peer *fakePC) {97+func (l *simulatedLink) flush(peer *fakePC) int {
98 l.mu.Lock()98 l.mu.Lock()
99 defer l.mu.Unlock()99 defer l.mu.Unlock()
100 now := l.clock.Now()100 now := l.clock.Now()
101+ delivered := 0
101 for len(l.queue) > 0 && !l.queue[0].due.After(now) {102 for len(l.queue) > 0 && !l.queue[0].due.After(now) {
102 q := l.queue[0]103 q := l.queue[0]
103 l.queue = l.queue[1:]104 l.queue = l.queue[1:]
104 l.queueBytes -= len(q.b)105 l.queueBytes -= len(q.b)
105- l.deliverLocked(peer, q.packet)106+ if l.deliverLocked(peer, q.packet) {
107+ delivered++
108+ }
106 }109 }
110+ return delivered
107 }111 }
108 112
109 func (l *simulatedLink) queuedBytes() int {113 func (l *simulatedLink) queuedBytes() int {
@@ -179,6 +183,7 @@ func (p *fakePC) ReadFrom(b []byte) (int, net.Addr, error) {
179 func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {183 func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
180 p.mu.Lock()184 p.mu.Lock()
181 closed := p.closed185 closed := p.closed
186+ afterWrite := p.afterWrite
182 p.mu.Unlock()187 p.mu.Unlock()
183 if closed {188 if closed {
184 return 0, errors.New("closed")189 return 0, errors.New("closed")
@@ -204,11 +209,29 @@ func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
204 }209 }
205 peer.mu.Unlock()210 peer.mu.Unlock()
206 }211 }
207- if p.afterWrite != nil {212+ if afterWrite != nil {
208- p.afterWrite()213+ afterWrite()
209 }214 }
210 return len(b), nil215 return len(b), nil
211 }216 }
217+func (p *fakePC) replaceAfterWrite(afterWrite func()) {
218+ p.mu.Lock()
219+ defer p.mu.Unlock()
220+ p.afterWrite = afterWrite
221+}
222+
223+func (p *fakePC) addAfterWrite(afterWrite func()) {
224+ p.mu.Lock()
225+ defer p.mu.Unlock()
226+ previous := p.afterWrite
227+ p.afterWrite = func() {
228+ if previous != nil {
229+ previous()
230+ }
231+ afterWrite()
232+ }
233+}
234+
212 func (p *fakePC) Close() error {235 func (p *fakePC) Close() error {
213 p.mu.Lock()236 p.mu.Lock()
214 defer p.mu.Unlock()237 defer p.mu.Unlock()
@@ -255,6 +278,15 @@ func awaitResult[T any](t *testing.T, ch <-chan T, context string) T {
255 }278 }
256 }279 }
257 280
281+func flushAndAwait(t *testing.T, link *simulatedLink, peer *fakePC, processed <-chan struct{}) int {
282+ t.Helper()
283+ delivered := link.flush(peer)
284+ for range delivered {
285+ awaitSignal(t, processed, "flushed packet processing")
286+ }
287+ return delivered
288+}
289+
258 // TestTransportFloodClassification drives authenticated datagrams through real290 // TestTransportFloodClassification drives authenticated datagrams through real
259 // transports. The metrics are transport state, not simulator bookkeeping.291 // transports. The metrics are transport state, not simulator bookkeeping.
260 func TestTransportFloodClassification(t *testing.T) {292 func TestTransportFloodClassification(t *testing.T) {
@@ -356,19 +388,65 @@ func TestTransportFloodClassification(t *testing.T) {
356 defer closeFloodTransports(a, b)388 defer closeFloodTransports(a, b)
357 waitForManualTimers(t, clk, 6)389 waitForManualTimers(t, clk, 6)
358 var contacted atomic.Bool390 var contacted atomic.Bool
391+ processed := make(chan struct{})
392+ primerWritten := make(chan struct{}, 1)
393+ primerACKScheduled := make(chan struct{}, 1)
394+ primerAcknowledged := make(chan struct{}, 1)
395+ ackQueued := make(chan struct{}, 128)
396+ ackScheduled := make(chan struct{}, 128)
397+ ackWritten := make(chan struct{}, 128)
398+ aPC.replaceAfterWrite(func() {
399+ select {
400+ case primerWritten <- struct{}{}:
401+ default:
402+ }
403+ })
404+ bPC.replaceAfterWrite(func() {
405+ select {
406+ case primerAcknowledged <- struct{}{}:
407+ default:
408+ }
409+ select {
410+ case ackWritten <- struct{}{}:
411+ default:
412+ }
413+ })
414+ a.mu.Lock()
415+ a.afterPacketProcessed = func() { processed <- struct{}{} }
416+ a.mu.Unlock()
359 b.mu.Lock()417 b.mu.Lock()
360 b.afterAuthenticatedPacket = func() { contacted.Store(true) }418 b.afterAuthenticatedPacket = func() { contacted.Store(true) }
419+ b.afterPacketProcessed = func() { processed <- struct{}{} }
420+ b.afterACKScheduled = func() {
421+ select {
422+ case primerACKScheduled <- struct{}{}:
423+ default:
424+ }
425+ select {
426+ case ackScheduled <- struct{}{}:
427+ default:
428+ }
429+ }
430+ b.afterACKQueued = func() { ackQueued <- struct{}{} }
361 b.mu.Unlock()431 b.mu.Unlock()
362 primer := ports.MarshalOutput(ports.Output{BaseStateNum: 0, NewStateNum: 1, Data: []byte("primer")})432 primer := ports.MarshalOutput(ports.Output{BaseStateNum: 0, NewStateNum: 1, Data: []byte("primer")})
363 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: primer}); err != nil {433 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: primer}); err != nil {
364 t.Fatal(err)434 t.Fatal(err)
365 }435 }
436+ awaitSignal(t, primerWritten, "primer write")
366 primerDrained := false437 primerDrained := false
438+ awaitingPrimerACK := false
367 for range 40 {439 for range 40 {
368 clk.advance(25 * time.Millisecond)440 clk.advance(25 * time.Millisecond)
369- forward.flush(bPC)441+ if awaitingPrimerACK {
370- reverse.flush(aPC)442+ awaitSignal(t, primerAcknowledged, "primer ACK write")
371- time.Sleep(100 * time.Microsecond)443+ awaitingPrimerACK = false
444+ }
445+ if flushAndAwait(t, forward, bPC, processed) > 0 {
446+ awaitSignal(t, primerACKScheduled, "primer ACK scheduling")
447+ awaitingPrimerACK = true
448+ }
449+ flushAndAwait(t, reverse, aPC, processed)
372 a.mu.Lock()450 a.mu.Lock()
373 primerDrained = len(a.pending) == 0451 primerDrained = len(a.pending) == 0
374 a.mu.Unlock()452 a.mu.Unlock()
@@ -379,9 +457,22 @@ func TestTransportFloodClassification(t *testing.T) {
379 if !primerDrained {457 if !primerDrained {
380 t.Fatal("clean RTT primer did not drain")458 t.Fatal("clean RTT primer did not drain")
381 }459 }
460+ <-ackQueued
461+ <-ackScheduled
382 a.mu.Lock()462 a.mu.Lock()
383 initialCwnd := a.congestion.cwndBytes463 initialCwnd := a.congestion.cwndBytes
384 a.mu.Unlock()464 a.mu.Unlock()
465+ dataPaceStarted := make(chan struct{}, 128)
466+ dataWritten := make(chan struct{}, 1)
467+ a.mu.Lock()
468+ a.beforeDataPace = func() { dataPaceStarted <- struct{}{} }
469+ a.mu.Unlock()
470+ aPC.addAfterWrite(func() {
471+ select {
472+ case dataWritten <- struct{}{}:
473+ default:
474+ }
475+ })
385 for state := 1; state < outputCount; state++ {476 for state := 1; state < outputCount; state++ {
386 payload := ports.MarshalOutput(ports.Output{477 payload := ports.MarshalOutput(ports.Output{
387 BaseStateNum: uint64(state),478 BaseStateNum: uint64(state),
@@ -392,8 +483,11 @@ func TestTransportFloodClassification(t *testing.T) {
392 t.Fatal(err)483 t.Fatal(err)
393 }484 }
394 }485 }
486+ awaitSignal(t, dataPaceStarted, "output pacing")
487+ awaitSignal(t, dataWritten, "first paced output write")
395 488
396 var maxContactAge time.Duration489 var maxContactAge time.Duration
490+ pendingACKWrites := 0
397 lossReducedCwnd := false491 lossReducedCwnd := false
398 latestState := uint64(0)492 latestState := uint64(0)
399 probed := false493 probed := false
@@ -401,9 +495,28 @@ func TestTransportFloodClassification(t *testing.T) {
401 probeWho := ""495 probeWho := ""
402 for range 2000 {496 for range 2000 {
403 clk.advance(25 * time.Millisecond)497 clk.advance(25 * time.Millisecond)
404- forward.flush(bPC)498+ for range pendingACKWrites {
405- reverse.flush(aPC)499+ awaitSignal(t, ackWritten, "cumulative ACK write")
406- time.Sleep(100 * time.Microsecond)500+ }
501+ pendingACKWrites = 0
502+ flushAndAwait(t, forward, bPC, processed)
503+ for {
504+ select {
505+ case <-ackQueued:
506+ awaitSignal(t, ackScheduled, "cumulative ACK scheduling")
507+ pendingACKWrites++
508+ default:
509+ goto ackRecordsDrained
510+ }
511+ }
512+ ackRecordsDrained:
513+ flushAndAwait(t, reverse, aPC, processed)
514+ a.mu.Lock()
515+ queuedOutput := len(a.outputQueue) > 0
516+ a.mu.Unlock()
517+ if queuedOutput {
518+ awaitSignal(t, dataPaceStarted, "output pacing")
519+ }
407 520
408 if forward.queuedBytes() > queueLimit || reverse.queuedBytes() > queueLimit {521 if forward.queuedBytes() > queueLimit || reverse.queuedBytes() > queueLimit {
409 t.Fatalf("queue exceeded bound: forward=%d reverse=%d limit=%d", forward.queuedBytes(), reverse.queuedBytes(), queueLimit)522 t.Fatalf("queue exceeded bound: forward=%d reverse=%d limit=%d", forward.queuedBytes(), reverse.queuedBytes(), queueLimit)
M internal/adapters/dgram/output_pacer.go +1 -1
@@ -35,7 +35,7 @@ func (t *Transport) outputPaceLoop() {
3535 copy(t.outputQueue, t.outputQueue[1:])
3636 t.outputQueue = t.outputQueue[:len(t.outputQueue)-1]
3737 t.mu.Unlock()
38- if err := t.queueDataJob(dataSendJob(q)); err != nil {
38+ if err := t.queueDataJob(q); err != nil {
3939 t.removeQueuedPending([]queuedSend{q}, err)
4040 t.outboundMu.Unlock()
4141 return
@@ -35,7 +35,7 @@ func (t *Transport) outputPaceLoop() {
35 copy(t.outputQueue, t.outputQueue[1:])35 copy(t.outputQueue, t.outputQueue[1:])
36 t.outputQueue = t.outputQueue[:len(t.outputQueue)-1]36 t.outputQueue = t.outputQueue[:len(t.outputQueue)-1]
37 t.mu.Unlock()37 t.mu.Unlock()
38- if err := t.queueDataJob(dataSendJob(q)); err != nil {38+ if err := t.queueDataJob(q); err != nil {
39 t.removeQueuedPending([]queuedSend{q}, err)39 t.removeQueuedPending([]queuedSend{q}, err)
40 t.outboundMu.Unlock()40 t.outboundMu.Unlock()
41 return41 return
M internal/adapters/dgram/transport.go +27 -2
@@ -216,6 +216,15 @@ type Transport struct {
216216 writeMu sync.Mutex
217217 // beforeDataPace is a test synchronization hook. It is read under mu.
218218 beforeDataPace func()
219+ // afterPacketProcessed is a test synchronization hook called after an
220+ // authenticated packet has been fully processed. It is read under mu.
221+ afterPacketProcessed func()
222+ // afterACKScheduled is a test synchronization hook called after the ACK
223+ // scheduler handles a wake. It is read under mu.
224+ afterACKScheduled func()
225+ // afterACKQueued is a test synchronization hook called after an ACK is
226+ // queued. It is read under mu.
227+ afterACKQueued func()
219228 // outboundMu orders first transmissions and paced batches. Retransmits use
220229 // writeMu directly because they do not establish new sequence order.
221230 outboundMu sync.Mutex
@@ -774,14 +783,26 @@ func (t *Transport) readLoop(pc net.PacketConn) {
774783 }
775784 if err != nil {
776785 t.notifyMalformedFragment()
786+ t.notifyPacketProcessed()
777787 continue
778788 }
779789 if !complete {
790+ t.notifyPacketProcessed()
780791 continue
781792 }
782793 t.recordCompleteRecord()
783794 t.handleRecord(payload)
784795 t.checkSilence()
796+ t.notifyPacketProcessed()
797+ }
798+}
799+
800+func (t *Transport) notifyPacketProcessed() {
801+ t.mu.Lock()
802+ afterPacketProcessed := t.afterPacketProcessed
803+ t.mu.Unlock()
804+ if afterPacketProcessed != nil {
805+ afterPacketProcessed()
785806 }
786807 }
787808
@@ -1266,13 +1287,17 @@ func (t *Transport) hopPacketConnOnce(generation uint64) {
12661287 t.hopGeneration = generation
12671288 t.mu.Unlock()
12681289 pc, err := rebind(old)
1290+
1291+ // Serialize the swap with writes. writePacket takes writeMu before mu, so
1292+ // preserve that order here while retiring old.
1293+ t.writeMu.Lock()
1294+ defer t.writeMu.Unlock()
1295+ t.mu.Lock()
12691296 if err != nil {
1270- t.mu.Lock()
12711297 t.rollbackHopLocked(generation)
12721298 t.mu.Unlock()
12731299 return
12741300 }
1275- t.mu.Lock()
12761301 if t.closed || t.pc != old || t.health.generation != generation || t.linkState != ports.LinkStateProbing {
12771302 t.rollbackHopLocked(generation)
12781303 t.mu.Unlock()
@@ -216,6 +216,15 @@ type Transport struct {
216 writeMu sync.Mutex216 writeMu sync.Mutex
217 // beforeDataPace is a test synchronization hook. It is read under mu.217 // beforeDataPace is a test synchronization hook. It is read under mu.
218 beforeDataPace func()218 beforeDataPace func()
219+ // afterPacketProcessed is a test synchronization hook called after an
220+ // authenticated packet has been fully processed. It is read under mu.
221+ afterPacketProcessed func()
222+ // afterACKScheduled is a test synchronization hook called after the ACK
223+ // scheduler handles a wake. It is read under mu.
224+ afterACKScheduled func()
225+ // afterACKQueued is a test synchronization hook called after an ACK is
226+ // queued. It is read under mu.
227+ afterACKQueued func()
219 // outboundMu orders first transmissions and paced batches. Retransmits use228 // outboundMu orders first transmissions and paced batches. Retransmits use
220 // writeMu directly because they do not establish new sequence order.229 // writeMu directly because they do not establish new sequence order.
221 outboundMu sync.Mutex230 outboundMu sync.Mutex
@@ -774,14 +783,26 @@ func (t *Transport) readLoop(pc net.PacketConn) {
774 }783 }
775 if err != nil {784 if err != nil {
776 t.notifyMalformedFragment()785 t.notifyMalformedFragment()
786+ t.notifyPacketProcessed()
777 continue787 continue
778 }788 }
779 if !complete {789 if !complete {
790+ t.notifyPacketProcessed()
780 continue791 continue
781 }792 }
782 t.recordCompleteRecord()793 t.recordCompleteRecord()
783 t.handleRecord(payload)794 t.handleRecord(payload)
784 t.checkSilence()795 t.checkSilence()
796+ t.notifyPacketProcessed()
797+ }
798+}
799+
800+func (t *Transport) notifyPacketProcessed() {
801+ t.mu.Lock()
802+ afterPacketProcessed := t.afterPacketProcessed
803+ t.mu.Unlock()
804+ if afterPacketProcessed != nil {
805+ afterPacketProcessed()
785 }806 }
786 }807 }
787 808
@@ -1266,13 +1287,17 @@ func (t *Transport) hopPacketConnOnce(generation uint64) {
1266 t.hopGeneration = generation1287 t.hopGeneration = generation
1267 t.mu.Unlock()1288 t.mu.Unlock()
1268 pc, err := rebind(old)1289 pc, err := rebind(old)
1290+
1291+ // Serialize the swap with writes. writePacket takes writeMu before mu, so
1292+ // preserve that order here while retiring old.
1293+ t.writeMu.Lock()
1294+ defer t.writeMu.Unlock()
1295+ t.mu.Lock()
1269 if err != nil {1296 if err != nil {
1270- t.mu.Lock()
1271 t.rollbackHopLocked(generation)1297 t.rollbackHopLocked(generation)
1272 t.mu.Unlock()1298 t.mu.Unlock()
1273 return1299 return
1274 }1300 }
1275- t.mu.Lock()
1276 if t.closed || t.pc != old || t.health.generation != generation || t.linkState != ports.LinkStateProbing {1301 if t.closed || t.pc != old || t.health.generation != generation || t.linkState != ports.LinkStateProbing {
1277 t.rollbackHopLocked(generation)1302 t.rollbackHopLocked(generation)
1278 t.mu.Unlock()1303 t.mu.Unlock()
M internal/adapters/dgram/transport_test.go +5 -1
@@ -2117,8 +2117,12 @@ func TestLinkHealthHeartbeatControlCannotMaskPendingACKStall(t *testing.T) {
21172117 t.Fatal(marshalErr)
21182118 }
21192119 tr.mu.Lock()
2120- pc := tr.pc.(*fakePC)
2120+ actual := tr.pc
2121+ pc, ok := actual.(*fakePC)
21212122 tr.mu.Unlock()
2123+ if !ok {
2124+ t.Fatalf("transport packet conn = %T, want *fakePC", actual)
2125+ }
21222126 pc.in <- packet{b: codec.Seal(2, counter, raw, nil), addr: testAddr("b")}
21232127 select {
21242128 case <-authenticated:
@@ -2117,8 +2117,12 @@ func TestLinkHealthHeartbeatControlCannotMaskPendingACKStall(t *testing.T) {
2117 t.Fatal(marshalErr)2117 t.Fatal(marshalErr)
2118 }2118 }
2119 tr.mu.Lock()2119 tr.mu.Lock()
2120- pc := tr.pc.(*fakePC)2120+ actual := tr.pc
2121+ pc, ok := actual.(*fakePC)
2121 tr.mu.Unlock()2122 tr.mu.Unlock()
2123+ if !ok {
2124+ t.Fatalf("transport packet conn = %T, want *fakePC", actual)
2125+ }
2122 pc.in <- packet{b: codec.Seal(2, counter, raw, nil), addr: testAddr("b")}2126 pc.in <- packet{b: codec.Seal(2, counter, raw, nil), addr: testAddr("b")}
2123 select {2127 select {
2124 case <-authenticated:2128 case <-authenticated:
M internal/usecase/daemon/output_state_stream.go +3 -3
@@ -22,10 +22,10 @@ type outputStateStream struct {
2222 maxOutstanding uint64
2323 }
2424
25-func newOutputStateStream(max ...uint8) *outputStateStream {
25+func newOutputStateStream(windowSize ...uint8) *outputStateStream {
2626 window := uint8(maxUnackedOutputStates)
27- if len(max) > 0 {
28- window = normalizeOutputWindow(max[0])
27+ if len(windowSize) > 0 {
28+ window = normalizeOutputWindow(windowSize[0])
2929 }
3030 return &outputStateStream{
3131 renderer: renderer.New(renderer.Capabilities{}),
@@ -22,10 +22,10 @@ type outputStateStream struct {
22 maxOutstanding uint6422 maxOutstanding uint64
23 }23 }
24 24
25-func newOutputStateStream(max ...uint8) *outputStateStream {25+func newOutputStateStream(windowSize ...uint8) *outputStateStream {
26 window := uint8(maxUnackedOutputStates)26 window := uint8(maxUnackedOutputStates)
27- if len(max) > 0 {27+ if len(windowSize) > 0 {
28- window = normalizeOutputWindow(max[0])28+ window = normalizeOutputWindow(windowSize[0])
29 }29 }
30 return &outputStateStream{30 return &outputStateStream{
31 renderer: renderer.New(renderer.Capabilities{}),31 renderer: renderer.New(renderer.Capabilities{}),
M scripts/debug-remote-attach_test.sh +3 -3
@@ -2,13 +2,13 @@
22 set -euo pipefail
33
44 repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5-help="$($repo_root/scripts/debug-remote-attach.sh --help 2>&1)"
5+help="$("$repo_root/scripts/debug-remote-attach.sh" --help 2>&1)"
66 grep -Fq -- '--udp-health' <<<"$help"
77
88 work="$(mktemp -d)"
99 trap 'rm -rf "$work"' EXIT
1010 mkdir -p "$work/bin" "$work/state/vev"
11-printf 'udp health peer=192.0.2.1 home=%s\n' "$HOME" >"$work/state/vev/vev-test.log"
11+printf 'udp health peer=192.0.2.1 home=%s/local\n' "$HOME" >"$work/state/vev/vev-test.log"
1212 cat >"$work/bin/ssh" <<'SSH'
1313 #!/usr/bin/env bash
1414 printf 'udp transport health peer=198.51.100.2 key=ssh-ed25519\nskmem:(r0,rb1,t0,tb1,f0,w0,o0,bl0,d0)\n'
@@ -22,7 +22,7 @@ test -n "$bundle"
2222 test -f "$bundle/local-after/udp-transport-health.out"
2323 test -f "$bundle/remote-after/udp-socket-memory.out"
2424 grep -Fq "$bundle/remote-after/udp-socket-memory.out" "$work/run.out"
25-if grep -R --exclude='*.raw' -E 'alice@mobile\.example\.com|192\.0\.2\.1|198\.51\.100\.2|ssh-ed25519' "$bundle"; then
25+if grep -R --exclude='*.raw' -F -e 'alice@mobile.example.com' -e '192.0.2.1' -e '198.51.100.2' -e 'ssh-ed25519' -e "$HOME/local" "$bundle"; then
2626 echo 'unredacted diagnostic value found' >&2
2727 exit 1
2828 fi
@@ -2,13 +2,13 @@
2 set -euo pipefail2 set -euo pipefail
3 3
4 repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"4 repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5-help="$($repo_root/scripts/debug-remote-attach.sh --help 2>&1)"5+help="$("$repo_root/scripts/debug-remote-attach.sh" --help 2>&1)"
6 grep -Fq -- '--udp-health' <<<"$help"6 grep -Fq -- '--udp-health' <<<"$help"
7 7
8 work="$(mktemp -d)"8 work="$(mktemp -d)"
9 trap 'rm -rf "$work"' EXIT9 trap 'rm -rf "$work"' EXIT
10 mkdir -p "$work/bin" "$work/state/vev"10 mkdir -p "$work/bin" "$work/state/vev"
11-printf 'udp health peer=192.0.2.1 home=%s\n' "$HOME" >"$work/state/vev/vev-test.log"11+printf 'udp health peer=192.0.2.1 home=%s/local\n' "$HOME" >"$work/state/vev/vev-test.log"
12 cat >"$work/bin/ssh" <<'SSH'12 cat >"$work/bin/ssh" <<'SSH'
13 #!/usr/bin/env bash13 #!/usr/bin/env bash
14 printf 'udp transport health peer=198.51.100.2 key=ssh-ed25519\nskmem:(r0,rb1,t0,tb1,f0,w0,o0,bl0,d0)\n'14 printf 'udp transport health peer=198.51.100.2 key=ssh-ed25519\nskmem:(r0,rb1,t0,tb1,f0,w0,o0,bl0,d0)\n'
@@ -22,7 +22,7 @@ test -n "$bundle"
22 test -f "$bundle/local-after/udp-transport-health.out"22 test -f "$bundle/local-after/udp-transport-health.out"
23 test -f "$bundle/remote-after/udp-socket-memory.out"23 test -f "$bundle/remote-after/udp-socket-memory.out"
24 grep -Fq "$bundle/remote-after/udp-socket-memory.out" "$work/run.out"24 grep -Fq "$bundle/remote-after/udp-socket-memory.out" "$work/run.out"
25-if grep -R --exclude='*.raw' -E 'alice@mobile\.example\.com|192\.0\.2\.1|198\.51\.100\.2|ssh-ed25519' "$bundle"; then25+if grep -R --exclude='*.raw' -F -e 'alice@mobile.example.com' -e '192.0.2.1' -e '198.51.100.2' -e 'ssh-ed25519' -e "$HOME/local" "$bundle"; then
26 echo 'unredacted diagnostic value found' >&226 echo 'unredacted diagnostic value found' >&2
27 exit 127 exit 1
28 fi28 fi

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