detached b9e591f

fix(dgram): make UDP diagnostics and fixtures deterministic

commit
b9e591f053e0d9b9b2f3d8bf2b04ae60f56f1b74
author
brice · · 2026-07-10 00:19 UTC
parent
96bc22d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +188 -63
M internal/adapters/dgram/diagnostics_test.go +73 -0
@@ -1,6 +1,7 @@
11 package dgram
22
33 import (
4+ "sync"
45 "testing"
56 "time"
67
@@ -50,6 +51,78 @@ func TestDiagnosticSnapshotSeparatesProgressAges(t *testing.T) {
5051 }
5152 }
5253
54+func TestDiagnosticSnapshotIsLockConsistentDuringConcurrentUpdate(t *testing.T) {
55+ now := time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC)
56+ clock := &snapshotClock{now: now, sampled: make(chan struct{}), release: make(chan struct{})}
57+ tr := &Transport{
58+ clock: clock,
59+ lastAuthenticatedPacket: now.Add(-time.Second),
60+ lastCompleteRecord: now.Add(-2 * time.Second),
61+ lastACKProgress: now.Add(-3 * time.Second),
62+ pending: make(map[uint64]*pending),
63+ diagnosticCh: make(chan Diagnostic, 1),
64+ }
65+
66+ go tr.emitDiagnostic()
67+ select {
68+ case <-clock.sampled:
69+ case <-time.After(time.Second):
70+ t.Fatal("diagnostic did not begin its clock snapshot")
71+ }
72+ updated := make(chan struct{})
73+ go func() {
74+ tr.mu.Lock()
75+ tr.lastAuthenticatedPacket = now.Add(time.Second)
76+ tr.lastCompleteRecord = now.Add(time.Second)
77+ tr.lastACKProgress = now.Add(time.Second)
78+ tr.retransmits = 1
79+ tr.mu.Unlock()
80+ close(updated)
81+ }()
82+ close(clock.release)
83+
84+ var d Diagnostic
85+ select {
86+ case d = <-tr.diagnosticCh:
87+ case <-time.After(time.Second):
88+ t.Fatal("diagnostic was not emitted")
89+ }
90+ select {
91+ case <-updated:
92+ case <-time.After(time.Second):
93+ t.Fatal("concurrent update was blocked")
94+ }
95+ if d.At != now || d.Retransmits != 0 {
96+ t.Fatalf("mixed diagnostic snapshot: %+v", d)
97+ }
98+ if d.SinceAuthenticatedPacket != time.Second || d.SinceCompleteRecord != 2*time.Second || d.SinceACKProgress != 3*time.Second {
99+ t.Fatalf("inconsistent diagnostic ages: %+v", d)
100+ }
101+ if d.SinceAuthenticatedPacket < 0 || d.SinceCompleteRecord < 0 || d.SinceACKProgress < 0 {
102+ t.Fatalf("negative diagnostic age: %+v", d)
103+ }
104+}
105+
106+type snapshotClock struct {
107+ mu sync.Mutex
108+ now time.Time
109+ sampled chan struct{}
110+ release chan struct{}
111+}
112+
113+func (c *snapshotClock) Now() time.Time {
114+ c.mu.Lock()
115+ defer c.mu.Unlock()
116+ select {
117+ case c.sampled <- struct{}{}:
118+ default:
119+ }
120+ <-c.release
121+ return c.now
122+}
123+
124+func (c *snapshotClock) NewTimer(time.Duration) ports.Timer { panic("not used") }
125+
53126 func TestDiagnosticObserverIsBounded(t *testing.T) {
54127 tr := &Transport{
55128 clock: fixedClock{now: time.Now()},
@@ -1,6 +1,7 @@
1 package dgram1 package dgram
2 2
3 import (3 import (
4+ "sync"
4 "testing"5 "testing"
5 "time"6 "time"
6 7
@@ -50,6 +51,78 @@ func TestDiagnosticSnapshotSeparatesProgressAges(t *testing.T) {
50 }51 }
51 }52 }
52 53
54+func TestDiagnosticSnapshotIsLockConsistentDuringConcurrentUpdate(t *testing.T) {
55+ now := time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC)
56+ clock := &snapshotClock{now: now, sampled: make(chan struct{}), release: make(chan struct{})}
57+ tr := &Transport{
58+ clock: clock,
59+ lastAuthenticatedPacket: now.Add(-time.Second),
60+ lastCompleteRecord: now.Add(-2 * time.Second),
61+ lastACKProgress: now.Add(-3 * time.Second),
62+ pending: make(map[uint64]*pending),
63+ diagnosticCh: make(chan Diagnostic, 1),
64+ }
65+
66+ go tr.emitDiagnostic()
67+ select {
68+ case <-clock.sampled:
69+ case <-time.After(time.Second):
70+ t.Fatal("diagnostic did not begin its clock snapshot")
71+ }
72+ updated := make(chan struct{})
73+ go func() {
74+ tr.mu.Lock()
75+ tr.lastAuthenticatedPacket = now.Add(time.Second)
76+ tr.lastCompleteRecord = now.Add(time.Second)
77+ tr.lastACKProgress = now.Add(time.Second)
78+ tr.retransmits = 1
79+ tr.mu.Unlock()
80+ close(updated)
81+ }()
82+ close(clock.release)
83+
84+ var d Diagnostic
85+ select {
86+ case d = <-tr.diagnosticCh:
87+ case <-time.After(time.Second):
88+ t.Fatal("diagnostic was not emitted")
89+ }
90+ select {
91+ case <-updated:
92+ case <-time.After(time.Second):
93+ t.Fatal("concurrent update was blocked")
94+ }
95+ if d.At != now || d.Retransmits != 0 {
96+ t.Fatalf("mixed diagnostic snapshot: %+v", d)
97+ }
98+ if d.SinceAuthenticatedPacket != time.Second || d.SinceCompleteRecord != 2*time.Second || d.SinceACKProgress != 3*time.Second {
99+ t.Fatalf("inconsistent diagnostic ages: %+v", d)
100+ }
101+ if d.SinceAuthenticatedPacket < 0 || d.SinceCompleteRecord < 0 || d.SinceACKProgress < 0 {
102+ t.Fatalf("negative diagnostic age: %+v", d)
103+ }
104+}
105+
106+type snapshotClock struct {
107+ mu sync.Mutex
108+ now time.Time
109+ sampled chan struct{}
110+ release chan struct{}
111+}
112+
113+func (c *snapshotClock) Now() time.Time {
114+ c.mu.Lock()
115+ defer c.mu.Unlock()
116+ select {
117+ case c.sampled <- struct{}{}:
118+ default:
119+ }
120+ <-c.release
121+ return c.now
122+}
123+
124+func (c *snapshotClock) NewTimer(time.Duration) ports.Timer { panic("not used") }
125+
53 func TestDiagnosticObserverIsBounded(t *testing.T) {126 func TestDiagnosticObserverIsBounded(t *testing.T) {
54 tr := &Transport{127 tr := &Transport{
55 clock: fixedClock{now: time.Now()},128 clock: fixedClock{now: time.Now()},
M internal/adapters/dgram/network_sim_test.go +56 -26
@@ -127,6 +127,7 @@ type fakePC struct {
127127 mu sync.Mutex
128128 closed bool
129129 in chan packet
130+ read chan struct{}
130131 peers map[string]*fakePC
131132 drop func([]byte, net.Addr) bool
132133 afterWrite func()
@@ -136,8 +137,8 @@ type fakePC struct {
136137 func newPair() (*fakePC, *fakePC) { return newPairWithLink(nil) }
137138
138139 func newPairWithLink(link *simulatedLink) (*fakePC, *fakePC) {
139- a := &fakePC{addr: testAddr("a"), in: make(chan packet, 100), peers: map[string]*fakePC{}, link: link}
140- b := &fakePC{addr: testAddr("b"), in: make(chan packet, 100), peers: map[string]*fakePC{}, link: link}
140+ a := &fakePC{addr: testAddr("a"), in: make(chan packet, 100), read: make(chan struct{}, 100), peers: map[string]*fakePC{}, link: link}
141+ b := &fakePC{addr: testAddr("b"), in: make(chan packet, 100), read: make(chan struct{}, 100), peers: map[string]*fakePC{}, link: link}
141142 a.peers["b"] = b
142143 b.peers["a"] = a
143144 return a, b
@@ -148,6 +149,10 @@ func (p *fakePC) ReadFrom(b []byte) (int, net.Addr, error) {
148149 return 0, nil, errors.New("closed")
149150 }
150151 copy(b, q.b)
152+ select {
153+ case p.read <- struct{}{}:
154+ default:
155+ }
151156 return len(q.b), q.addr, nil
152157 }
153158 func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
@@ -172,7 +177,10 @@ func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
172177 peer.mu.Unlock()
173178 return 0, errors.New("peer closed")
174179 }
175- peer.in <- packet{append([]byte(nil), b...), p.addr}
180+ select {
181+ case peer.in <- packet{append([]byte(nil), b...), p.addr}:
182+ default:
183+ }
176184 peer.mu.Unlock()
177185 }
178186 if p.afterWrite != nil {
@@ -196,6 +204,31 @@ func (p *fakePC) SetWriteDeadline(time.Time) error { return nil }
196204
197205 func key() []byte { return bytes.Repeat([]byte{1}, pdgram.KeySize) }
198206
207+const fixtureWaitTimeout = time.Second
208+
209+// awaitSignal gives fixture barriers a diagnostic deadline. The deadline is a
210+// hang guard only; tests advance behavior with the manual clock and barriers.
211+func awaitSignal(t *testing.T, ch <-chan struct{}, context string) {
212+ t.Helper()
213+ select {
214+ case <-ch:
215+ case <-time.After(fixtureWaitTimeout):
216+ t.Fatalf("timed out waiting for %s", context)
217+ }
218+}
219+
220+func awaitResult[T any](t *testing.T, ch <-chan T, context string) T {
221+ t.Helper()
222+ select {
223+ case result := <-ch:
224+ return result
225+ case <-time.After(fixtureWaitTimeout):
226+ t.Fatalf("timed out waiting for %s", context)
227+ var zero T
228+ return zero
229+ }
230+}
231+
199232 // TestTransportFloodClassification drives authenticated datagrams through real
200233 // transports. The metrics are transport state, not simulator bookkeeping.
201234 func TestTransportFloodClassification(t *testing.T) {
@@ -221,6 +254,10 @@ func TestTransportFloodClassification(t *testing.T) {
221254 }
222255 a, b := newFloodTransports(t, aPC, bPC, clk, 128)
223256 defer closeFloodTransports(a, b)
257+ contacted := make(chan struct{}, 2)
258+ b.mu.Lock()
259+ b.afterAuthenticatedPacket = func() { contacted <- struct{}{} }
260+ b.mu.Unlock()
224261 start := clk.Now()
225262 clk.advance(time.Nanosecond)
226263 for range 2 {
@@ -228,7 +265,8 @@ func TestTransportFloodClassification(t *testing.T) {
228265 t.Fatal(err)
229266 }
230267 }
231- floodEventually(t, func() bool { return floodState(b).packet && len(dropped) == 2 })
268+ awaitSignal(t, contacted, "first authenticated fragment")
269+ awaitSignal(t, contacted, "second authenticated fragment")
232270 state := floodState(b)
233271 if !state.packet || state.record || floodState(a).ack {
234272 t.Fatalf("contact=%v complete=%v ack=%v, want contact only", state.packet, state.record, floodState(a).ack)
@@ -245,6 +283,10 @@ func TestTransportFloodClassification(t *testing.T) {
245283 aPC, bPC := newPairWithLink(link)
246284 a, b := newFloodTransports(t, aPC, bPC, clk, 128)
247285 defer closeFloodTransports(a, b)
286+ contacted := make(chan struct{}, 1)
287+ b.mu.Lock()
288+ b.afterAuthenticatedPacket = func() { contacted <- struct{}{} }
289+ b.mu.Unlock()
248290 a.mu.Lock()
249291 a.rto = time.Hour // Keep the fake-clock queue case about bandwidth, not retries.
250292 a.mu.Unlock()
@@ -257,7 +299,7 @@ func TestTransportFloodClassification(t *testing.T) {
257299 }
258300 clk.advance(10 * time.Second)
259301 link.flush(bPC)
260- floodEventually(t, func() bool { return floodState(b).packet })
302+ awaitSignal(t, contacted, "queued authenticated fragment")
261303 if got := link.queuedBytes(); got != 0 {
262304 t.Fatalf("queued bytes after fake-clock release=%d, want 0", got)
263305 }
@@ -322,7 +364,7 @@ func TestTransportFloodClassification(t *testing.T) {
322364 writesMu.Unlock()
323365 if secondWrite {
324366 retransmitBlocked <- struct{}{}
325- <-releaseRetransmit
367+ awaitSignal(t, releaseRetransmit, "retransmit release")
326368 }
327369 return false
328370 }
@@ -339,7 +381,7 @@ func TestTransportFloodClassification(t *testing.T) {
339381 }
340382 defer closeFloodTransports(a, b)
341383 for range 4 { // Both transports install resend and heartbeat timers.
342- <-clk.timerCreated
384+ awaitSignal(t, clk.timerCreated, "transport timer creation")
343385 }
344386
345387 // Two fresh, fragmented output records become overdue while their real
@@ -358,11 +400,11 @@ func TestTransportFloodClassification(t *testing.T) {
358400 }
359401 }
360402 for range 2 {
361- <-acksDropped
403+ awaitSignal(t, acksDropped, "dropped cumulative ACK")
362404 }
363405
364406 clk.advance(10 * time.Millisecond)
365- <-retransmitBlocked // resendPending is synchronously inside writePacket.
407+ awaitSignal(t, retransmitBlocked, "retransmit pacing barrier") // resendPending is synchronously inside writePacket.
366408 if got := floodState(a).retransmits; got < 2 {
367409 t.Fatalf("retransmits=%d, want both overdue large records selected", got)
368410 }
@@ -374,13 +416,13 @@ func TestTransportFloodClassification(t *testing.T) {
374416 a.outputNext = time.Time{}
375417 a.beforeDataPace = func() {
376418 close(freshAtPace)
377- <-allowFreshPace
419+ awaitSignal(t, allowFreshPace, "fresh pacing release")
378420 }
379421 a.mu.Unlock()
380422 go func() {
381423 freshReturned <- a.Send(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 500)})
382424 }()
383- <-freshAtPace
425+ awaitSignal(t, freshAtPace, "fresh output pacing barrier")
384426 a.mu.Lock()
385427 a.beforeDataPace = nil
386428 a.mu.Unlock()
@@ -399,10 +441,10 @@ func TestTransportFloodClassification(t *testing.T) {
399441 }
400442
401443 close(releaseRetransmit)
402- if err := <-freshReturned; err != nil {
444+ if err := awaitResult(t, freshReturned, "fresh output completion"); err != nil {
403445 t.Fatal(err)
404446 }
405- <-probeSent
447+ awaitSignal(t, probeSent, "heartbeat probe")
406448 writesMu.Lock()
407449 defer writesMu.Unlock()
408450 if writes[1] < 2 || writes[2] < 2 || writes[3] == 0 {
@@ -448,18 +490,6 @@ func closeFloodTransports(a, b *Transport) {
448490 _ = b.Close()
449491 }
450492
451-func floodEventually(t *testing.T, condition func() bool) {
452- t.Helper()
453- deadline := time.Now().Add(time.Second)
454- for time.Now().Before(deadline) {
455- if condition() {
456- return
457- }
458- time.Sleep(time.Millisecond)
459- }
460- t.Fatal("condition was not met")
461-}
462-
463493 func TestMalformedAuthenticatedFragmentDoesNotCountAsContact(t *testing.T) {
464494 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
465495 aPC, bPC := newPair()
@@ -476,7 +506,7 @@ func TestMalformedAuthenticatedFragmentDoesNotCountAsContact(t *testing.T) {
476506 if _, err := aPC.WriteTo(codec.Seal(1, 999, []byte("not a fragment"), nil), bPC.addr); err != nil {
477507 t.Fatal(err)
478508 }
479- time.Sleep(10 * time.Millisecond)
509+ awaitSignal(t, bPC.read, "malformed datagram read")
480510 b.mu.Lock()
481511 gotPacket, gotRecord := b.lastAuthenticatedPacket, b.lastCompleteRecord
482512 b.mu.Unlock()
@@ -127,6 +127,7 @@ type fakePC struct {
127 mu sync.Mutex127 mu sync.Mutex
128 closed bool128 closed bool
129 in chan packet129 in chan packet
130+ read chan struct{}
130 peers map[string]*fakePC131 peers map[string]*fakePC
131 drop func([]byte, net.Addr) bool132 drop func([]byte, net.Addr) bool
132 afterWrite func()133 afterWrite func()
@@ -136,8 +137,8 @@ type fakePC struct {
136 func newPair() (*fakePC, *fakePC) { return newPairWithLink(nil) }137 func newPair() (*fakePC, *fakePC) { return newPairWithLink(nil) }
137 138
138 func newPairWithLink(link *simulatedLink) (*fakePC, *fakePC) {139 func newPairWithLink(link *simulatedLink) (*fakePC, *fakePC) {
139- a := &fakePC{addr: testAddr("a"), in: make(chan packet, 100), peers: map[string]*fakePC{}, link: link}140+ a := &fakePC{addr: testAddr("a"), in: make(chan packet, 100), read: make(chan struct{}, 100), peers: map[string]*fakePC{}, link: link}
140- b := &fakePC{addr: testAddr("b"), in: make(chan packet, 100), peers: map[string]*fakePC{}, link: link}141+ b := &fakePC{addr: testAddr("b"), in: make(chan packet, 100), read: make(chan struct{}, 100), peers: map[string]*fakePC{}, link: link}
141 a.peers["b"] = b142 a.peers["b"] = b
142 b.peers["a"] = a143 b.peers["a"] = a
143 return a, b144 return a, b
@@ -148,6 +149,10 @@ func (p *fakePC) ReadFrom(b []byte) (int, net.Addr, error) {
148 return 0, nil, errors.New("closed")149 return 0, nil, errors.New("closed")
149 }150 }
150 copy(b, q.b)151 copy(b, q.b)
152+ select {
153+ case p.read <- struct{}{}:
154+ default:
155+ }
151 return len(q.b), q.addr, nil156 return len(q.b), q.addr, nil
152 }157 }
153 func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {158 func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
@@ -172,7 +177,10 @@ func (p *fakePC) WriteTo(b []byte, addr net.Addr) (int, error) {
172 peer.mu.Unlock()177 peer.mu.Unlock()
173 return 0, errors.New("peer closed")178 return 0, errors.New("peer closed")
174 }179 }
175- peer.in <- packet{append([]byte(nil), b...), p.addr}180+ select {
181+ case peer.in <- packet{append([]byte(nil), b...), p.addr}:
182+ default:
183+ }
176 peer.mu.Unlock()184 peer.mu.Unlock()
177 }185 }
178 if p.afterWrite != nil {186 if p.afterWrite != nil {
@@ -196,6 +204,31 @@ func (p *fakePC) SetWriteDeadline(time.Time) error { return nil }
196 204
197 func key() []byte { return bytes.Repeat([]byte{1}, pdgram.KeySize) }205 func key() []byte { return bytes.Repeat([]byte{1}, pdgram.KeySize) }
198 206
207+const fixtureWaitTimeout = time.Second
208+
209+// awaitSignal gives fixture barriers a diagnostic deadline. The deadline is a
210+// hang guard only; tests advance behavior with the manual clock and barriers.
211+func awaitSignal(t *testing.T, ch <-chan struct{}, context string) {
212+ t.Helper()
213+ select {
214+ case <-ch:
215+ case <-time.After(fixtureWaitTimeout):
216+ t.Fatalf("timed out waiting for %s", context)
217+ }
218+}
219+
220+func awaitResult[T any](t *testing.T, ch <-chan T, context string) T {
221+ t.Helper()
222+ select {
223+ case result := <-ch:
224+ return result
225+ case <-time.After(fixtureWaitTimeout):
226+ t.Fatalf("timed out waiting for %s", context)
227+ var zero T
228+ return zero
229+ }
230+}
231+
199 // TestTransportFloodClassification drives authenticated datagrams through real232 // TestTransportFloodClassification drives authenticated datagrams through real
200 // transports. The metrics are transport state, not simulator bookkeeping.233 // transports. The metrics are transport state, not simulator bookkeeping.
201 func TestTransportFloodClassification(t *testing.T) {234 func TestTransportFloodClassification(t *testing.T) {
@@ -221,6 +254,10 @@ func TestTransportFloodClassification(t *testing.T) {
221 }254 }
222 a, b := newFloodTransports(t, aPC, bPC, clk, 128)255 a, b := newFloodTransports(t, aPC, bPC, clk, 128)
223 defer closeFloodTransports(a, b)256 defer closeFloodTransports(a, b)
257+ contacted := make(chan struct{}, 2)
258+ b.mu.Lock()
259+ b.afterAuthenticatedPacket = func() { contacted <- struct{}{} }
260+ b.mu.Unlock()
224 start := clk.Now()261 start := clk.Now()
225 clk.advance(time.Nanosecond)262 clk.advance(time.Nanosecond)
226 for range 2 {263 for range 2 {
@@ -228,7 +265,8 @@ func TestTransportFloodClassification(t *testing.T) {
228 t.Fatal(err)265 t.Fatal(err)
229 }266 }
230 }267 }
231- floodEventually(t, func() bool { return floodState(b).packet && len(dropped) == 2 })268+ awaitSignal(t, contacted, "first authenticated fragment")
269+ awaitSignal(t, contacted, "second authenticated fragment")
232 state := floodState(b)270 state := floodState(b)
233 if !state.packet || state.record || floodState(a).ack {271 if !state.packet || state.record || floodState(a).ack {
234 t.Fatalf("contact=%v complete=%v ack=%v, want contact only", state.packet, state.record, floodState(a).ack)272 t.Fatalf("contact=%v complete=%v ack=%v, want contact only", state.packet, state.record, floodState(a).ack)
@@ -245,6 +283,10 @@ func TestTransportFloodClassification(t *testing.T) {
245 aPC, bPC := newPairWithLink(link)283 aPC, bPC := newPairWithLink(link)
246 a, b := newFloodTransports(t, aPC, bPC, clk, 128)284 a, b := newFloodTransports(t, aPC, bPC, clk, 128)
247 defer closeFloodTransports(a, b)285 defer closeFloodTransports(a, b)
286+ contacted := make(chan struct{}, 1)
287+ b.mu.Lock()
288+ b.afterAuthenticatedPacket = func() { contacted <- struct{}{} }
289+ b.mu.Unlock()
248 a.mu.Lock()290 a.mu.Lock()
249 a.rto = time.Hour // Keep the fake-clock queue case about bandwidth, not retries.291 a.rto = time.Hour // Keep the fake-clock queue case about bandwidth, not retries.
250 a.mu.Unlock()292 a.mu.Unlock()
@@ -257,7 +299,7 @@ func TestTransportFloodClassification(t *testing.T) {
257 }299 }
258 clk.advance(10 * time.Second)300 clk.advance(10 * time.Second)
259 link.flush(bPC)301 link.flush(bPC)
260- floodEventually(t, func() bool { return floodState(b).packet })302+ awaitSignal(t, contacted, "queued authenticated fragment")
261 if got := link.queuedBytes(); got != 0 {303 if got := link.queuedBytes(); got != 0 {
262 t.Fatalf("queued bytes after fake-clock release=%d, want 0", got)304 t.Fatalf("queued bytes after fake-clock release=%d, want 0", got)
263 }305 }
@@ -322,7 +364,7 @@ func TestTransportFloodClassification(t *testing.T) {
322 writesMu.Unlock()364 writesMu.Unlock()
323 if secondWrite {365 if secondWrite {
324 retransmitBlocked <- struct{}{}366 retransmitBlocked <- struct{}{}
325- <-releaseRetransmit367+ awaitSignal(t, releaseRetransmit, "retransmit release")
326 }368 }
327 return false369 return false
328 }370 }
@@ -339,7 +381,7 @@ func TestTransportFloodClassification(t *testing.T) {
339 }381 }
340 defer closeFloodTransports(a, b)382 defer closeFloodTransports(a, b)
341 for range 4 { // Both transports install resend and heartbeat timers.383 for range 4 { // Both transports install resend and heartbeat timers.
342- <-clk.timerCreated384+ awaitSignal(t, clk.timerCreated, "transport timer creation")
343 }385 }
344 386
345 // Two fresh, fragmented output records become overdue while their real387 // Two fresh, fragmented output records become overdue while their real
@@ -358,11 +400,11 @@ func TestTransportFloodClassification(t *testing.T) {
358 }400 }
359 }401 }
360 for range 2 {402 for range 2 {
361- <-acksDropped403+ awaitSignal(t, acksDropped, "dropped cumulative ACK")
362 }404 }
363 405
364 clk.advance(10 * time.Millisecond)406 clk.advance(10 * time.Millisecond)
365- <-retransmitBlocked // resendPending is synchronously inside writePacket.407+ awaitSignal(t, retransmitBlocked, "retransmit pacing barrier") // resendPending is synchronously inside writePacket.
366 if got := floodState(a).retransmits; got < 2 {408 if got := floodState(a).retransmits; got < 2 {
367 t.Fatalf("retransmits=%d, want both overdue large records selected", got)409 t.Fatalf("retransmits=%d, want both overdue large records selected", got)
368 }410 }
@@ -374,13 +416,13 @@ func TestTransportFloodClassification(t *testing.T) {
374 a.outputNext = time.Time{}416 a.outputNext = time.Time{}
375 a.beforeDataPace = func() {417 a.beforeDataPace = func() {
376 close(freshAtPace)418 close(freshAtPace)
377- <-allowFreshPace419+ awaitSignal(t, allowFreshPace, "fresh pacing release")
378 }420 }
379 a.mu.Unlock()421 a.mu.Unlock()
380 go func() {422 go func() {
381 freshReturned <- a.Send(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 500)})423 freshReturned <- a.Send(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 500)})
382 }()424 }()
383- <-freshAtPace425+ awaitSignal(t, freshAtPace, "fresh output pacing barrier")
384 a.mu.Lock()426 a.mu.Lock()
385 a.beforeDataPace = nil427 a.beforeDataPace = nil
386 a.mu.Unlock()428 a.mu.Unlock()
@@ -399,10 +441,10 @@ func TestTransportFloodClassification(t *testing.T) {
399 }441 }
400 442
401 close(releaseRetransmit)443 close(releaseRetransmit)
402- if err := <-freshReturned; err != nil {444+ if err := awaitResult(t, freshReturned, "fresh output completion"); err != nil {
403 t.Fatal(err)445 t.Fatal(err)
404 }446 }
405- <-probeSent447+ awaitSignal(t, probeSent, "heartbeat probe")
406 writesMu.Lock()448 writesMu.Lock()
407 defer writesMu.Unlock()449 defer writesMu.Unlock()
408 if writes[1] < 2 || writes[2] < 2 || writes[3] == 0 {450 if writes[1] < 2 || writes[2] < 2 || writes[3] == 0 {
@@ -448,18 +490,6 @@ func closeFloodTransports(a, b *Transport) {
448 _ = b.Close()490 _ = b.Close()
449 }491 }
450 492
451-func floodEventually(t *testing.T, condition func() bool) {
452- t.Helper()
453- deadline := time.Now().Add(time.Second)
454- for time.Now().Before(deadline) {
455- if condition() {
456- return
457- }
458- time.Sleep(time.Millisecond)
459- }
460- t.Fatal("condition was not met")
461-}
462-
463 func TestMalformedAuthenticatedFragmentDoesNotCountAsContact(t *testing.T) {493 func TestMalformedAuthenticatedFragmentDoesNotCountAsContact(t *testing.T) {
464 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))494 clk := newManualClock(time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC))
465 aPC, bPC := newPair()495 aPC, bPC := newPair()
@@ -476,7 +506,7 @@ func TestMalformedAuthenticatedFragmentDoesNotCountAsContact(t *testing.T) {
476 if _, err := aPC.WriteTo(codec.Seal(1, 999, []byte("not a fragment"), nil), bPC.addr); err != nil {506 if _, err := aPC.WriteTo(codec.Seal(1, 999, []byte("not a fragment"), nil), bPC.addr); err != nil {
477 t.Fatal(err)507 t.Fatal(err)
478 }508 }
479- time.Sleep(10 * time.Millisecond)509+ awaitSignal(t, bPC.read, "malformed datagram read")
480 b.mu.Lock()510 b.mu.Lock()
481 gotPacket, gotRecord := b.lastAuthenticatedPacket, b.lastCompleteRecord511 gotPacket, gotRecord := b.lastAuthenticatedPacket, b.lastCompleteRecord
482 b.mu.Unlock()512 b.mu.Unlock()
M internal/adapters/dgram/transport.go +59 -37
@@ -153,34 +153,37 @@ type Transport struct {
153153 lastACKProgress time.Time
154154 retransmits uint64
155155 reassemblyInflight int
156- observe DiagnosticObserver
157- diagnosticCh chan Diagnostic
158- heartbeat time.Duration
159- resendAfter time.Duration
160- maxResendAfter time.Duration
161- maxResendPerTick int
162- srtt time.Duration
163- rttvar time.Duration
164- rto time.Duration
165- writeTimeout time.Duration
166- degradedAfter time.Duration
167- probeAfter time.Duration
168- offlineAfter time.Duration
169- deadAfter time.Duration
170- maxPending int
171- maxPendingWait time.Duration
172- maxRecvBuffer int
173- clock ports.Clock
174- linkState ports.LinkState
175- linkEvents chan ports.LinkEvent
176- probeWait map[uint64]chan struct{}
177- sendWake chan struct{}
178- probeReply chan uint64
179- rebind func(net.PacketConn) (net.PacketConn, error)
180- hoppedOffline bool
181- outputQueue []queuedSend
182- outputWake chan struct{}
183- outputNext time.Time
156+ // afterAuthenticatedPacket is a test synchronization hook. It runs after
157+ // authenticated-contact state has been committed.
158+ afterAuthenticatedPacket func()
159+ observe DiagnosticObserver
160+ diagnosticCh chan Diagnostic
161+ heartbeat time.Duration
162+ resendAfter time.Duration
163+ maxResendAfter time.Duration
164+ maxResendPerTick int
165+ srtt time.Duration
166+ rttvar time.Duration
167+ rto time.Duration
168+ writeTimeout time.Duration
169+ degradedAfter time.Duration
170+ probeAfter time.Duration
171+ offlineAfter time.Duration
172+ deadAfter time.Duration
173+ maxPending int
174+ maxPendingWait time.Duration
175+ maxRecvBuffer int
176+ clock ports.Clock
177+ linkState ports.LinkState
178+ linkEvents chan ports.LinkEvent
179+ probeWait map[uint64]chan struct{}
180+ sendWake chan struct{}
181+ probeReply chan uint64
182+ rebind func(net.PacketConn) (net.PacketConn, error)
183+ hoppedOffline bool
184+ outputQueue []queuedSend
185+ outputWake chan struct{}
186+ outputNext time.Time
184187
185188 recvMu sync.Mutex
186189 replay *pdgram.ReplayWindow
@@ -709,7 +712,11 @@ func (t *Transport) recordAuthenticatedFragment(inflight int) {
709712 t.mu.Lock()
710713 t.lastAuthenticatedPacket = t.clock.Now()
711714 t.reassemblyInflight = inflight
715+ after := t.afterAuthenticatedPacket
712716 t.mu.Unlock()
717+ if after != nil {
718+ after()
719+ }
713720 }
714721
715722 func (t *Transport) recordCompleteRecord() {
@@ -1132,32 +1139,47 @@ func (t *Transport) emitDiagnostic() {
11321139 if t.diagnosticCh == nil {
11331140 return
11341141 }
1135- now := t.clock.Now()
11361142 t.mu.Lock()
1143+ now := t.clock.Now()
1144+ lastAuthenticatedPacket := t.lastAuthenticatedPacket
1145+ lastCompleteRecord := t.lastCompleteRecord
1146+ lastACKProgress := t.lastACKProgress
11371147 pendingBytes := 0
11381148 for _, p := range t.pending {
11391149 if p != nil {
11401150 pendingBytes += len(p.frame.Payload)
11411151 }
11421152 }
1153+ state := t.linkState
1154+ pendingRecords := len(t.pending)
1155+ retransmits := t.retransmits
1156+ reassemblyInflight := t.reassemblyInflight
1157+ t.mu.Unlock()
1158+
11431159 d := Diagnostic{
11441160 At: now,
1145- State: t.linkState,
1146- SinceAuthenticatedPacket: now.Sub(t.lastAuthenticatedPacket),
1147- SinceCompleteRecord: now.Sub(t.lastCompleteRecord),
1148- SinceACKProgress: now.Sub(t.lastACKProgress),
1149- PendingRecords: len(t.pending),
1161+ State: state,
1162+ SinceAuthenticatedPacket: diagnosticAge(now, lastAuthenticatedPacket),
1163+ SinceCompleteRecord: diagnosticAge(now, lastCompleteRecord),
1164+ SinceACKProgress: diagnosticAge(now, lastACKProgress),
1165+ PendingRecords: pendingRecords,
11501166 PendingBytes: pendingBytes,
1151- Retransmits: t.retransmits,
1152- ReassemblyInflight: t.reassemblyInflight,
1167+ Retransmits: retransmits,
1168+ ReassemblyInflight: reassemblyInflight,
11531169 }
1154- t.mu.Unlock()
11551170 select {
11561171 case t.diagnosticCh <- d:
11571172 default:
11581173 }
11591174 }
11601175
1176+func diagnosticAge(now, then time.Time) time.Duration {
1177+ if then.After(now) {
1178+ return 0
1179+ }
1180+ return now.Sub(then)
1181+}
1182+
11611183 func (t *Transport) closeWithError(err error) {
11621184 t.mu.Lock()
11631185 if t.closed {
@@ -153,34 +153,37 @@ type Transport struct {
153 lastACKProgress time.Time153 lastACKProgress time.Time
154 retransmits uint64154 retransmits uint64
155 reassemblyInflight int155 reassemblyInflight int
156- observe DiagnosticObserver156+ // afterAuthenticatedPacket is a test synchronization hook. It runs after
157- diagnosticCh chan Diagnostic157+ // authenticated-contact state has been committed.
158- heartbeat time.Duration158+ afterAuthenticatedPacket func()
159- resendAfter time.Duration159+ observe DiagnosticObserver
160- maxResendAfter time.Duration160+ diagnosticCh chan Diagnostic
161- maxResendPerTick int161+ heartbeat time.Duration
162- srtt time.Duration162+ resendAfter time.Duration
163- rttvar time.Duration163+ maxResendAfter time.Duration
164- rto time.Duration164+ maxResendPerTick int
165- writeTimeout time.Duration165+ srtt time.Duration
166- degradedAfter time.Duration166+ rttvar time.Duration
167- probeAfter time.Duration167+ rto time.Duration
168- offlineAfter time.Duration168+ writeTimeout time.Duration
169- deadAfter time.Duration169+ degradedAfter time.Duration
170- maxPending int170+ probeAfter time.Duration
171- maxPendingWait time.Duration171+ offlineAfter time.Duration
172- maxRecvBuffer int172+ deadAfter time.Duration
173- clock ports.Clock173+ maxPending int
174- linkState ports.LinkState174+ maxPendingWait time.Duration
175- linkEvents chan ports.LinkEvent175+ maxRecvBuffer int
176- probeWait map[uint64]chan struct{}176+ clock ports.Clock
177- sendWake chan struct{}177+ linkState ports.LinkState
178- probeReply chan uint64178+ linkEvents chan ports.LinkEvent
179- rebind func(net.PacketConn) (net.PacketConn, error)179+ probeWait map[uint64]chan struct{}
180- hoppedOffline bool180+ sendWake chan struct{}
181- outputQueue []queuedSend181+ probeReply chan uint64
182- outputWake chan struct{}182+ rebind func(net.PacketConn) (net.PacketConn, error)
183- outputNext time.Time183+ hoppedOffline bool
184+ outputQueue []queuedSend
185+ outputWake chan struct{}
186+ outputNext time.Time
184 187
185 recvMu sync.Mutex188 recvMu sync.Mutex
186 replay *pdgram.ReplayWindow189 replay *pdgram.ReplayWindow
@@ -709,7 +712,11 @@ func (t *Transport) recordAuthenticatedFragment(inflight int) {
709 t.mu.Lock()712 t.mu.Lock()
710 t.lastAuthenticatedPacket = t.clock.Now()713 t.lastAuthenticatedPacket = t.clock.Now()
711 t.reassemblyInflight = inflight714 t.reassemblyInflight = inflight
715+ after := t.afterAuthenticatedPacket
712 t.mu.Unlock()716 t.mu.Unlock()
717+ if after != nil {
718+ after()
719+ }
713 }720 }
714 721
715 func (t *Transport) recordCompleteRecord() {722 func (t *Transport) recordCompleteRecord() {
@@ -1132,32 +1139,47 @@ func (t *Transport) emitDiagnostic() {
1132 if t.diagnosticCh == nil {1139 if t.diagnosticCh == nil {
1133 return1140 return
1134 }1141 }
1135- now := t.clock.Now()
1136 t.mu.Lock()1142 t.mu.Lock()
1143+ now := t.clock.Now()
1144+ lastAuthenticatedPacket := t.lastAuthenticatedPacket
1145+ lastCompleteRecord := t.lastCompleteRecord
1146+ lastACKProgress := t.lastACKProgress
1137 pendingBytes := 01147 pendingBytes := 0
1138 for _, p := range t.pending {1148 for _, p := range t.pending {
1139 if p != nil {1149 if p != nil {
1140 pendingBytes += len(p.frame.Payload)1150 pendingBytes += len(p.frame.Payload)
1141 }1151 }
1142 }1152 }
1153+ state := t.linkState
1154+ pendingRecords := len(t.pending)
1155+ retransmits := t.retransmits
1156+ reassemblyInflight := t.reassemblyInflight
1157+ t.mu.Unlock()
1158+
1143 d := Diagnostic{1159 d := Diagnostic{
1144 At: now,1160 At: now,
1145- State: t.linkState,1161+ State: state,
1146- SinceAuthenticatedPacket: now.Sub(t.lastAuthenticatedPacket),1162+ SinceAuthenticatedPacket: diagnosticAge(now, lastAuthenticatedPacket),
1147- SinceCompleteRecord: now.Sub(t.lastCompleteRecord),1163+ SinceCompleteRecord: diagnosticAge(now, lastCompleteRecord),
1148- SinceACKProgress: now.Sub(t.lastACKProgress),1164+ SinceACKProgress: diagnosticAge(now, lastACKProgress),
1149- PendingRecords: len(t.pending),1165+ PendingRecords: pendingRecords,
1150 PendingBytes: pendingBytes,1166 PendingBytes: pendingBytes,
1151- Retransmits: t.retransmits,1167+ Retransmits: retransmits,
1152- ReassemblyInflight: t.reassemblyInflight,1168+ ReassemblyInflight: reassemblyInflight,
1153 }1169 }
1154- t.mu.Unlock()
1155 select {1170 select {
1156 case t.diagnosticCh <- d:1171 case t.diagnosticCh <- d:
1157 default:1172 default:
1158 }1173 }
1159 }1174 }
1160 1175
1176+func diagnosticAge(now, then time.Time) time.Duration {
1177+ if then.After(now) {
1178+ return 0
1179+ }
1180+ return now.Sub(then)
1181+}
1182+
1161 func (t *Transport) closeWithError(err error) {1183 func (t *Transport) closeWithError(err error) {
1162 t.mu.Lock()1184 t.mu.Lock()
1163 if t.closed {1185 if t.closed {

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