detached d25aab8

fix(dgram): gate resend until first send completes

commit
d25aab8cc8f407d8bd0c3ac65a9b63632174a3c0
author
brice · · 2026-07-09 18:47 UTC
parent
54a6817
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +63 -36
M internal/adapters/dgram/output_pacer.go +13 -0
@@ -108,6 +108,7 @@ func (t *Transport) sendQueuedData(q queuedSend) error {
108108 t.removePending(q.seq, q.reliable)
109109 return err
110110 }
111+ t.markPendingReady(q.seq, q.reliable)
111112 return nil
112113 }
113114
@@ -120,6 +121,18 @@ func (t *Transport) markPendingSent(seq uint64, reliable bool) {
120121 if p := t.pending[seq]; p != nil && p.first.IsZero() {
121122 p.first = now
122123 p.last = now
124+ p.initialInFlight = true
125+ }
126+ t.mu.Unlock()
127+}
128+
129+func (t *Transport) markPendingReady(seq uint64, reliable bool) {
130+ if !reliable {
131+ return
132+ }
133+ t.mu.Lock()
134+ if p := t.pending[seq]; p != nil {
135+ p.initialInFlight = false
123136 }
124137 t.mu.Unlock()
125138 }
@@ -108,6 +108,7 @@ func (t *Transport) sendQueuedData(q queuedSend) error {
108 t.removePending(q.seq, q.reliable)108 t.removePending(q.seq, q.reliable)
109 return err109 return err
110 }110 }
111+ t.markPendingReady(q.seq, q.reliable)
111 return nil112 return nil
112 }113 }
113 114
@@ -120,6 +121,18 @@ func (t *Transport) markPendingSent(seq uint64, reliable bool) {
120 if p := t.pending[seq]; p != nil && p.first.IsZero() {121 if p := t.pending[seq]; p != nil && p.first.IsZero() {
121 p.first = now122 p.first = now
122 p.last = now123 p.last = now
124+ p.initialInFlight = true
125+ }
126+ t.mu.Unlock()
127+}
128+
129+func (t *Transport) markPendingReady(seq uint64, reliable bool) {
130+ if !reliable {
131+ return
132+ }
133+ t.mu.Lock()
134+ if p := t.pending[seq]; p != nil {
135+ p.initialInFlight = false
123 }136 }
124 t.mu.Unlock()137 t.mu.Unlock()
125 }138 }
M internal/adapters/dgram/transport.go +9 -6
@@ -189,11 +189,12 @@ type Transport struct {
189189 }
190190
191191 type pending struct {
192- frame ports.Frame
193- first time.Time
194- last time.Time
195- attempts int
196- retransmitted bool
192+ frame ports.Frame
193+ first time.Time
194+ last time.Time
195+ initialInFlight bool
196+ attempts int
197+ retransmitted bool
197198 }
198199
199200 func NewTransport(pc net.PacketConn, peer net.Addr, key []byte, sendDir, recvDir uint32) (*Transport, error) {
@@ -282,6 +283,7 @@ func (t *Transport) Send(f ports.Frame) error {
282283 t.removePending(seq, reliable)
283284 return err
284285 }
286+ t.markPendingReady(seq, reliable)
285287 return nil
286288 }
287289 queued := t.flushQueuedOutputLocked()
@@ -298,6 +300,7 @@ func (t *Transport) Send(f ports.Frame) error {
298300 t.removePending(seq, reliable)
299301 return err
300302 }
303+ t.markPendingReady(seq, reliable)
301304 return nil
302305 }
303306
@@ -776,7 +779,7 @@ func (t *Transport) resendPending() {
776779 break
777780 }
778781 p := t.pending[seq]
779- if p == nil || p.last.IsZero() || now.Sub(p.last) < t.resendDelayLocked(p) {
782+ if p == nil || p.last.IsZero() || p.initialInFlight || now.Sub(p.last) < t.resendDelayLocked(p) {
780783 continue
781784 }
782785 p.last = now
@@ -189,11 +189,12 @@ type Transport struct {
189 }189 }
190 190
191 type pending struct {191 type pending struct {
192- frame ports.Frame192+ frame ports.Frame
193- first time.Time193+ first time.Time
194- last time.Time194+ last time.Time
195- attempts int195+ initialInFlight bool
196- retransmitted bool196+ attempts int
197+ retransmitted bool
197 }198 }
198 199
199 func NewTransport(pc net.PacketConn, peer net.Addr, key []byte, sendDir, recvDir uint32) (*Transport, error) {200 func NewTransport(pc net.PacketConn, peer net.Addr, key []byte, sendDir, recvDir uint32) (*Transport, error) {
@@ -282,6 +283,7 @@ func (t *Transport) Send(f ports.Frame) error {
282 t.removePending(seq, reliable)283 t.removePending(seq, reliable)
283 return err284 return err
284 }285 }
286+ t.markPendingReady(seq, reliable)
285 return nil287 return nil
286 }288 }
287 queued := t.flushQueuedOutputLocked()289 queued := t.flushQueuedOutputLocked()
@@ -298,6 +300,7 @@ func (t *Transport) Send(f ports.Frame) error {
298 t.removePending(seq, reliable)300 t.removePending(seq, reliable)
299 return err301 return err
300 }302 }
303+ t.markPendingReady(seq, reliable)
301 return nil304 return nil
302 }305 }
303 306
@@ -776,7 +779,7 @@ func (t *Transport) resendPending() {
776 break779 break
777 }780 }
778 p := t.pending[seq]781 p := t.pending[seq]
779- if p == nil || p.last.IsZero() || now.Sub(p.last) < t.resendDelayLocked(p) {782+ if p == nil || p.last.IsZero() || p.initialInFlight || now.Sub(p.last) < t.resendDelayLocked(p) {
780 continue783 continue
781 }784 }
782 p.last = now785 p.last = now
M internal/adapters/dgram/transport_test.go +41 -30
@@ -171,12 +171,11 @@ func (c fixedClock) NewTimer(d time.Duration) ports.Timer {
171171 type manualClock struct {
172172 mu sync.Mutex
173173 now time.Time
174- timers chan *manualTimer
175- active int
174+ timers map[*manualTimer]struct{}
176175 }
177176
178177 func newManualClock(now time.Time) *manualClock {
179- return &manualClock{now: now, timers: make(chan *manualTimer, 16)}
178+ return &manualClock{now: now, timers: make(map[*manualTimer]struct{})}
180179 }
181180
182181 func (c *manualClock) Now() time.Time {
@@ -187,11 +186,9 @@ func (c *manualClock) Now() time.Time {
187186
188187 func (c *manualClock) NewTimer(d time.Duration) ports.Timer {
189188 c.mu.Lock()
190- deadline := c.now.Add(d)
191- c.active++
189+ tm := &manualTimer{clock: c, c: make(chan time.Time, 1), deadline: c.now.Add(d), active: true}
190+ c.timers[tm] = struct{}{}
192191 c.mu.Unlock()
193- tm := &manualTimer{clock: c, c: make(chan time.Time, 1), deadline: deadline, active: true}
194- c.timers <- tm
195192 return tm
196193 }
197194
@@ -199,14 +196,13 @@ func (c *manualClock) advance(d time.Duration) {
199196 c.mu.Lock()
200197 c.now = c.now.Add(d)
201198 now := c.now
199+ timers := make([]*manualTimer, 0, len(c.timers))
200+ for tm := range c.timers {
201+ timers = append(timers, tm)
202+ }
202203 c.mu.Unlock()
203- n := len(c.timers)
204- for range n {
205- tm := <-c.timers
206- if fired, keep := tm.fireIfDue(now); fired || !keep {
207- continue
208- }
209- c.timers <- tm
204+ for _, tm := range timers {
205+ tm.fireIfDue(now)
210206 }
211207 }
212208
@@ -223,13 +219,8 @@ func (t *manualTimer) Reset(d time.Duration) bool {
223219 wasActive := t.active
224220 t.deadline = t.clock.now.Add(d)
225221 t.active = true
226- if !wasActive {
227- t.clock.active++
228- }
222+ t.clock.timers[t] = struct{}{}
229223 t.clock.mu.Unlock()
230- if !wasActive {
231- t.clock.timers <- t
232- }
233224 return wasActive
234225 }
235226 func (t *manualTimer) Stop() bool {
@@ -237,29 +228,28 @@ func (t *manualTimer) Stop() bool {
237228 defer t.clock.mu.Unlock()
238229 wasActive := t.active
239230 t.active = false
240- if wasActive {
241- t.clock.active--
242- }
231+ delete(t.clock.timers, t)
243232 return wasActive
244233 }
245-func (t *manualTimer) fireIfDue(now time.Time) (fired bool, keep bool) {
234+func (t *manualTimer) fireIfDue(now time.Time) bool {
246235 t.clock.mu.Lock()
247236 if !t.active {
237+ delete(t.clock.timers, t)
248238 t.clock.mu.Unlock()
249- return false, false
239+ return false
250240 }
251241 if now.Before(t.deadline) {
252242 t.clock.mu.Unlock()
253- return false, true
243+ return false
254244 }
255245 t.active = false
256- t.clock.active--
246+ delete(t.clock.timers, t)
257247 t.clock.mu.Unlock()
258248 select {
259249 case t.c <- now:
260250 default:
261251 }
262- return true, false
252+ return true
263253 }
264254
265255 func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
@@ -267,7 +257,7 @@ func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
267257 deadline := time.Now().Add(time.Second)
268258 for time.Now().Before(deadline) {
269259 clk.mu.Lock()
270- active := clk.active
260+ active := len(clk.timers)
271261 clk.mu.Unlock()
272262 if active >= n {
273263 return
@@ -275,7 +265,7 @@ func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
275265 time.Sleep(time.Millisecond)
276266 }
277267 clk.mu.Lock()
278- active := clk.active
268+ active := len(clk.timers)
279269 clk.mu.Unlock()
280270 t.Fatalf("manual timers=%d, want at least %d", active, n)
281271 }
@@ -353,6 +343,27 @@ func TestResendSkipsQueuedOutputBeforeFirstWireAttempt(t *testing.T) {
353343 }
354344 }
355345
346+func TestResendSkipsOutputDuringFirstWireAttempt(t *testing.T) {
347+ tr, writes := newResendTestTransport(t, 10)
348+ now := tr.clock.Now()
349+ tr.pending[1] = &pending{
350+ frame: ports.Frame{Type: ports.MsgOutput, Payload: []byte("sending")},
351+ first: now.Add(-time.Second),
352+ last: now.Add(-time.Second),
353+ initialInFlight: true,
354+ }
355+
356+ tr.resendPending()
357+ if got := writes.Load(); got != 0 {
358+ t.Fatalf("writes=%d, want first wire attempt excluded from resend", got)
359+ }
360+ tr.pending[1].initialInFlight = false
361+ tr.resendPending()
362+ if got := writes.Load(); got != 1 {
363+ t.Fatalf("writes=%d, want resend after first wire attempt completed", got)
364+ }
365+}
366+
356367 func TestOutputPaceDelayUsesSRTTHalfClamped(t *testing.T) {
357368 tr, _ := newResendTestTransport(t, 10)
358369 tests := []struct {
@@ -171,12 +171,11 @@ func (c fixedClock) NewTimer(d time.Duration) ports.Timer {
171 type manualClock struct {171 type manualClock struct {
172 mu sync.Mutex172 mu sync.Mutex
173 now time.Time173 now time.Time
174- timers chan *manualTimer174+ timers map[*manualTimer]struct{}
175- active int
176 }175 }
177 176
178 func newManualClock(now time.Time) *manualClock {177 func newManualClock(now time.Time) *manualClock {
179- return &manualClock{now: now, timers: make(chan *manualTimer, 16)}178+ return &manualClock{now: now, timers: make(map[*manualTimer]struct{})}
180 }179 }
181 180
182 func (c *manualClock) Now() time.Time {181 func (c *manualClock) Now() time.Time {
@@ -187,11 +186,9 @@ func (c *manualClock) Now() time.Time {
187 186
188 func (c *manualClock) NewTimer(d time.Duration) ports.Timer {187 func (c *manualClock) NewTimer(d time.Duration) ports.Timer {
189 c.mu.Lock()188 c.mu.Lock()
190- deadline := c.now.Add(d)189+ tm := &manualTimer{clock: c, c: make(chan time.Time, 1), deadline: c.now.Add(d), active: true}
191- c.active++190+ c.timers[tm] = struct{}{}
192 c.mu.Unlock()191 c.mu.Unlock()
193- tm := &manualTimer{clock: c, c: make(chan time.Time, 1), deadline: deadline, active: true}
194- c.timers <- tm
195 return tm192 return tm
196 }193 }
197 194
@@ -199,14 +196,13 @@ func (c *manualClock) advance(d time.Duration) {
199 c.mu.Lock()196 c.mu.Lock()
200 c.now = c.now.Add(d)197 c.now = c.now.Add(d)
201 now := c.now198 now := c.now
199+ timers := make([]*manualTimer, 0, len(c.timers))
200+ for tm := range c.timers {
201+ timers = append(timers, tm)
202+ }
202 c.mu.Unlock()203 c.mu.Unlock()
203- n := len(c.timers)204+ for _, tm := range timers {
204- for range n {205+ tm.fireIfDue(now)
205- tm := <-c.timers
206- if fired, keep := tm.fireIfDue(now); fired || !keep {
207- continue
208- }
209- c.timers <- tm
210 }206 }
211 }207 }
212 208
@@ -223,13 +219,8 @@ func (t *manualTimer) Reset(d time.Duration) bool {
223 wasActive := t.active219 wasActive := t.active
224 t.deadline = t.clock.now.Add(d)220 t.deadline = t.clock.now.Add(d)
225 t.active = true221 t.active = true
226- if !wasActive {222+ t.clock.timers[t] = struct{}{}
227- t.clock.active++
228- }
229 t.clock.mu.Unlock()223 t.clock.mu.Unlock()
230- if !wasActive {
231- t.clock.timers <- t
232- }
233 return wasActive224 return wasActive
234 }225 }
235 func (t *manualTimer) Stop() bool {226 func (t *manualTimer) Stop() bool {
@@ -237,29 +228,28 @@ func (t *manualTimer) Stop() bool {
237 defer t.clock.mu.Unlock()228 defer t.clock.mu.Unlock()
238 wasActive := t.active229 wasActive := t.active
239 t.active = false230 t.active = false
240- if wasActive {231+ delete(t.clock.timers, t)
241- t.clock.active--
242- }
243 return wasActive232 return wasActive
244 }233 }
245-func (t *manualTimer) fireIfDue(now time.Time) (fired bool, keep bool) {234+func (t *manualTimer) fireIfDue(now time.Time) bool {
246 t.clock.mu.Lock()235 t.clock.mu.Lock()
247 if !t.active {236 if !t.active {
237+ delete(t.clock.timers, t)
248 t.clock.mu.Unlock()238 t.clock.mu.Unlock()
249- return false, false239+ return false
250 }240 }
251 if now.Before(t.deadline) {241 if now.Before(t.deadline) {
252 t.clock.mu.Unlock()242 t.clock.mu.Unlock()
253- return false, true243+ return false
254 }244 }
255 t.active = false245 t.active = false
256- t.clock.active--246+ delete(t.clock.timers, t)
257 t.clock.mu.Unlock()247 t.clock.mu.Unlock()
258 select {248 select {
259 case t.c <- now:249 case t.c <- now:
260 default:250 default:
261 }251 }
262- return true, false252+ return true
263 }253 }
264 254
265 func waitForManualTimers(t *testing.T, clk *manualClock, n int) {255 func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
@@ -267,7 +257,7 @@ func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
267 deadline := time.Now().Add(time.Second)257 deadline := time.Now().Add(time.Second)
268 for time.Now().Before(deadline) {258 for time.Now().Before(deadline) {
269 clk.mu.Lock()259 clk.mu.Lock()
270- active := clk.active260+ active := len(clk.timers)
271 clk.mu.Unlock()261 clk.mu.Unlock()
272 if active >= n {262 if active >= n {
273 return263 return
@@ -275,7 +265,7 @@ func waitForManualTimers(t *testing.T, clk *manualClock, n int) {
275 time.Sleep(time.Millisecond)265 time.Sleep(time.Millisecond)
276 }266 }
277 clk.mu.Lock()267 clk.mu.Lock()
278- active := clk.active268+ active := len(clk.timers)
279 clk.mu.Unlock()269 clk.mu.Unlock()
280 t.Fatalf("manual timers=%d, want at least %d", active, n)270 t.Fatalf("manual timers=%d, want at least %d", active, n)
281 }271 }
@@ -353,6 +343,27 @@ func TestResendSkipsQueuedOutputBeforeFirstWireAttempt(t *testing.T) {
353 }343 }
354 }344 }
355 345
346+func TestResendSkipsOutputDuringFirstWireAttempt(t *testing.T) {
347+ tr, writes := newResendTestTransport(t, 10)
348+ now := tr.clock.Now()
349+ tr.pending[1] = &pending{
350+ frame: ports.Frame{Type: ports.MsgOutput, Payload: []byte("sending")},
351+ first: now.Add(-time.Second),
352+ last: now.Add(-time.Second),
353+ initialInFlight: true,
354+ }
355+
356+ tr.resendPending()
357+ if got := writes.Load(); got != 0 {
358+ t.Fatalf("writes=%d, want first wire attempt excluded from resend", got)
359+ }
360+ tr.pending[1].initialInFlight = false
361+ tr.resendPending()
362+ if got := writes.Load(); got != 1 {
363+ t.Fatalf("writes=%d, want resend after first wire attempt completed", got)
364+ }
365+}
366+
356 func TestOutputPaceDelayUsesSRTTHalfClamped(t *testing.T) {367 func TestOutputPaceDelayUsesSRTTHalfClamped(t *testing.T) {
357 tr, _ := newResendTestTransport(t, 10)368 tr, _ := newResendTestTransport(t, 10)
358 tests := []struct {369 tests := []struct {

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