detached 56875c7

fix(remote): preserve transport identity on sends

commit
56875c746291d8c62772bace3d07d70527d0e01c
author
brice · · 2026-07-04 12:55 UTC
parent
9da1afc
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +76 -26
M internal/adapters/dgram/transport.go +8 -6
@@ -288,26 +288,27 @@ func (t *Transport) handleRecord(p []byte) {
288288 return
289289 }
290290 if reliable {
291- t.sendAck(seq)
292- t.enqueueReliable(seq, f)
291+ if t.enqueueReliable(seq, f) {
292+ t.sendAck(seq)
293+ }
293294 return
294295 }
295296 t.deliver(f)
296297 }
297298 }
298299
299-func (t *Transport) enqueueReliable(seq uint64, f ports.Frame) {
300+func (t *Transport) enqueueReliable(seq uint64, f ports.Frame) bool {
300301 t.deliverMu.Lock()
301302 defer t.deliverMu.Unlock()
302303 if seq < t.nextRecvSeq {
303- return
304+ return true
304305 }
305306 if _, exists := t.recvBuf[seq]; exists {
306- return
307+ return true
307308 }
308309 if len(t.recvBuf) >= maxRecvBuffer {
309310 if seq != t.nextRecvSeq {
310- return
311+ return false
311312 }
312313 for bufferedSeq := range t.recvBuf {
313314 delete(t.recvBuf, bufferedSeq)
@@ -316,6 +317,7 @@ func (t *Transport) enqueueReliable(seq uint64, f ports.Frame) {
316317 }
317318 t.recvBuf[seq] = f
318319 t.deliverCond.Signal()
320+ return true
319321 }
320322
321323 func (t *Transport) deliveryLoop() {
@@ -288,26 +288,27 @@ func (t *Transport) handleRecord(p []byte) {
288 return288 return
289 }289 }
290 if reliable {290 if reliable {
291- t.sendAck(seq)291+ if t.enqueueReliable(seq, f) {
292- t.enqueueReliable(seq, f)292+ t.sendAck(seq)
293+ }
293 return294 return
294 }295 }
295 t.deliver(f)296 t.deliver(f)
296 }297 }
297 }298 }
298 299
299-func (t *Transport) enqueueReliable(seq uint64, f ports.Frame) {300+func (t *Transport) enqueueReliable(seq uint64, f ports.Frame) bool {
300 t.deliverMu.Lock()301 t.deliverMu.Lock()
301 defer t.deliverMu.Unlock()302 defer t.deliverMu.Unlock()
302 if seq < t.nextRecvSeq {303 if seq < t.nextRecvSeq {
303- return304+ return true
304 }305 }
305 if _, exists := t.recvBuf[seq]; exists {306 if _, exists := t.recvBuf[seq]; exists {
306- return307+ return true
307 }308 }
308 if len(t.recvBuf) >= maxRecvBuffer {309 if len(t.recvBuf) >= maxRecvBuffer {
309 if seq != t.nextRecvSeq {310 if seq != t.nextRecvSeq {
310- return311+ return false
311 }312 }
312 for bufferedSeq := range t.recvBuf {313 for bufferedSeq := range t.recvBuf {
313 delete(t.recvBuf, bufferedSeq)314 delete(t.recvBuf, bufferedSeq)
@@ -316,6 +317,7 @@ func (t *Transport) enqueueReliable(seq uint64, f ports.Frame) {
316 }317 }
317 t.recvBuf[seq] = f318 t.recvBuf[seq] = f
318 t.deliverCond.Signal()319 t.deliverCond.Signal()
320+ return true
319 }321 }
320 322
321 func (t *Transport) deliveryLoop() {323 func (t *Transport) deliveryLoop() {
M internal/adapters/dgram/transport_test.go +29 -0
@@ -337,6 +337,35 @@ func TestReliableRecvBufferBoundedForFarFutureSequences(t *testing.T) {
337337 }
338338 }
339339
340+func TestReliableFullRecvBufferDoesNotAckDroppedFarFutureFrame(t *testing.T) {
341+ aPC, bPC := newPair()
342+ codec, err := pdgram.NewCodec(key())
343+ if err != nil {
344+ t.Fatal(err)
345+ }
346+ tr := &Transport{pc: aPC, peer: testAddr("b"), codec: codec, sendDir: 1, mtu: pdgram.DefaultMTU, nextRecvSeq: 1, recvBuf: make(map[uint64]ports.Frame), done: make(chan struct{})}
347+ tr.deliverCond = sync.NewCond(&tr.deliverMu)
348+ defer func() { _ = aPC.Close() }()
349+ defer func() { _ = bPC.Close() }()
350+
351+ for i := 0; i < maxRecvBuffer; i++ {
352+ tr.recvBuf[uint64(1000+i)] = ports.Frame{Type: ports.MsgOutput, Payload: []byte{byte(i)}}
353+ }
354+ tr.handleRecord(encodeData(9999, true, ports.Frame{Type: ports.MsgOutput, Payload: []byte("dropped")}))
355+ select {
356+ case pkt := <-bPC.in:
357+ t.Fatalf("unexpected ACK packet for dropped frame: %x", pkt.b)
358+ case <-time.After(25 * time.Millisecond):
359+ }
360+
361+ tr.handleRecord(encodeData(1, true, ports.Frame{Type: ports.MsgOutput, Payload: []byte("next")}))
362+ select {
363+ case <-bPC.in:
364+ case <-time.After(time.Second):
365+ t.Fatal("expected ACK for buffered contiguous frame")
366+ }
367+}
368+
340369 func recvMaybe(tr *Transport, d time.Duration) (ports.Frame, bool) {
341370 ch := make(chan ports.Frame, 1)
342371 go func() { f, _ := tr.Recv(); ch <- f }()
@@ -337,6 +337,35 @@ func TestReliableRecvBufferBoundedForFarFutureSequences(t *testing.T) {
337 }337 }
338 }338 }
339 339
340+func TestReliableFullRecvBufferDoesNotAckDroppedFarFutureFrame(t *testing.T) {
341+ aPC, bPC := newPair()
342+ codec, err := pdgram.NewCodec(key())
343+ if err != nil {
344+ t.Fatal(err)
345+ }
346+ tr := &Transport{pc: aPC, peer: testAddr("b"), codec: codec, sendDir: 1, mtu: pdgram.DefaultMTU, nextRecvSeq: 1, recvBuf: make(map[uint64]ports.Frame), done: make(chan struct{})}
347+ tr.deliverCond = sync.NewCond(&tr.deliverMu)
348+ defer func() { _ = aPC.Close() }()
349+ defer func() { _ = bPC.Close() }()
350+
351+ for i := 0; i < maxRecvBuffer; i++ {
352+ tr.recvBuf[uint64(1000+i)] = ports.Frame{Type: ports.MsgOutput, Payload: []byte{byte(i)}}
353+ }
354+ tr.handleRecord(encodeData(9999, true, ports.Frame{Type: ports.MsgOutput, Payload: []byte("dropped")}))
355+ select {
356+ case pkt := <-bPC.in:
357+ t.Fatalf("unexpected ACK packet for dropped frame: %x", pkt.b)
358+ case <-time.After(25 * time.Millisecond):
359+ }
360+
361+ tr.handleRecord(encodeData(1, true, ports.Frame{Type: ports.MsgOutput, Payload: []byte("next")}))
362+ select {
363+ case <-bPC.in:
364+ case <-time.After(time.Second):
365+ t.Fatal("expected ACK for buffered contiguous frame")
366+ }
367+}
368+
340 func recvMaybe(tr *Transport, d time.Duration) (ports.Frame, bool) {369 func recvMaybe(tr *Transport, d time.Duration) (ports.Frame, bool) {
341 ch := make(chan ports.Frame, 1)370 ch := make(chan ports.Frame, 1)
342 go func() { f, _ := tr.Recv(); ch <- f }()371 go func() { f, _ := tr.Recv(); ch <- f }()
M internal/usecase/daemon/client.go +39 -20
@@ -208,13 +208,18 @@ func (ac *attachedClient) nextOutputFrameLocked(b []byte) ports.Frame {
208208
209209 // send serialises a frame onto the client's transport.
210210 func (ac *attachedClient) send(f ports.Frame) error {
211+ _, err := ac.sendTransport(f)
212+ return err
213+}
214+
215+func (ac *attachedClient) sendTransport(f ports.Frame) (ports.Transport, error) {
211216 ac.sendMu.Lock()
212217 defer ac.sendMu.Unlock()
213218 tr := ac.transport()
214219 if tr == nil {
215- return errors.New("client transport is nil")
220+ return nil, errors.New("client transport is nil")
216221 }
217- return tr.Send(f)
222+ return tr, tr.Send(f)
218223 }
219224
220225 // boundedSend sends f to ac with a deadline watchdog: if the send (including
@@ -227,7 +232,15 @@ func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) {
227232 }
228233
229234 func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error {
230- return d.boundedSendWith(ac, func() error { return ac.send(f) })
235+ tr, err := d.boundedSendWith(ac, func(capture func(ports.Transport)) error {
236+ tr, err := ac.sendTransport(f)
237+ capture(tr)
238+ return err
239+ })
240+ if err != nil && errors.Is(err, errSendTimedOut) {
241+ _ = ac.closeCapturedTransport(tr)
242+ }
243+ return err
231244 }
232245
233246 func (d *Daemon) boundedSendOutputErr(ac *attachedClient, b []byte) error {
@@ -236,45 +249,51 @@ func (d *Daemon) boundedSendOutputErr(ac *attachedClient, b []byte) error {
236249 }
237250
238251 func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (ports.Transport, error) {
239- var (
240- usedMu sync.Mutex
241- used ports.Transport
242- )
243- err := d.boundedSendWith(ac, func() error {
252+ return d.boundedSendWith(ac, func(capture func(ports.Transport)) error {
244253 ac.sendMu.Lock()
245254 defer ac.sendMu.Unlock()
246255 tr := ac.transport()
247- usedMu.Lock()
248- used = tr
249- usedMu.Unlock()
256+ capture(tr)
250257 if tr == nil {
251258 return errors.New("client transport is nil")
252259 }
253260 return tr.Send(ac.nextOutputFrameLocked(b))
254261 })
255- usedMu.Lock()
256- defer usedMu.Unlock()
257- return used, err
258262 }
259263
260-func (d *Daemon) boundedSendWith(ac *attachedClient, send func() error) error {
264+var errSendTimedOut = errors.New("send timed out")
265+
266+func (d *Daemon) boundedSendWith(ac *attachedClient, send func(capture func(ports.Transport)) error) (ports.Transport, error) {
261267 timer := d.clock.NewTimer(detachNotifyTimeout)
262268 result := make(chan error, 1)
269+ var (
270+ capturedMu sync.Mutex
271+ captured ports.Transport
272+ )
273+ capture := func(tr ports.Transport) {
274+ capturedMu.Lock()
275+ captured = tr
276+ capturedMu.Unlock()
277+ }
278+ capturedTransport := func() ports.Transport {
279+ capturedMu.Lock()
280+ defer capturedMu.Unlock()
281+ return captured
282+ }
263283 go func() {
264- result <- send()
284+ result <- send(capture)
265285 }()
266286 select {
267287 case err := <-result:
268288 timer.Stop()
269- return err
289+ return capturedTransport(), err
270290 case <-timer.C():
271291 select {
272292 case err := <-result:
273- return err
293+ return capturedTransport(), err
274294 default:
275295 }
276- _ = ac.closeTransport()
277- return errors.New("send timed out")
296+ return capturedTransport(), errSendTimedOut
278297 }
279298 }
280299
@@ -208,13 +208,18 @@ func (ac *attachedClient) nextOutputFrameLocked(b []byte) ports.Frame {
208 208
209 // send serialises a frame onto the client's transport.209 // send serialises a frame onto the client's transport.
210 func (ac *attachedClient) send(f ports.Frame) error {210 func (ac *attachedClient) send(f ports.Frame) error {
211+ _, err := ac.sendTransport(f)
212+ return err
213+}
214+
215+func (ac *attachedClient) sendTransport(f ports.Frame) (ports.Transport, error) {
211 ac.sendMu.Lock()216 ac.sendMu.Lock()
212 defer ac.sendMu.Unlock()217 defer ac.sendMu.Unlock()
213 tr := ac.transport()218 tr := ac.transport()
214 if tr == nil {219 if tr == nil {
215- return errors.New("client transport is nil")220+ return nil, errors.New("client transport is nil")
216 }221 }
217- return tr.Send(f)222+ return tr, tr.Send(f)
218 }223 }
219 224
220 // boundedSend sends f to ac with a deadline watchdog: if the send (including225 // boundedSend sends f to ac with a deadline watchdog: if the send (including
@@ -227,7 +232,15 @@ func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) {
227 }232 }
228 233
229 func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error {234 func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error {
230- return d.boundedSendWith(ac, func() error { return ac.send(f) })235+ tr, err := d.boundedSendWith(ac, func(capture func(ports.Transport)) error {
236+ tr, err := ac.sendTransport(f)
237+ capture(tr)
238+ return err
239+ })
240+ if err != nil && errors.Is(err, errSendTimedOut) {
241+ _ = ac.closeCapturedTransport(tr)
242+ }
243+ return err
231 }244 }
232 245
233 func (d *Daemon) boundedSendOutputErr(ac *attachedClient, b []byte) error {246 func (d *Daemon) boundedSendOutputErr(ac *attachedClient, b []byte) error {
@@ -236,45 +249,51 @@ func (d *Daemon) boundedSendOutputErr(ac *attachedClient, b []byte) error {
236 }249 }
237 250
238 func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (ports.Transport, error) {251 func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (ports.Transport, error) {
239- var (252+ return d.boundedSendWith(ac, func(capture func(ports.Transport)) error {
240- usedMu sync.Mutex
241- used ports.Transport
242- )
243- err := d.boundedSendWith(ac, func() error {
244 ac.sendMu.Lock()253 ac.sendMu.Lock()
245 defer ac.sendMu.Unlock()254 defer ac.sendMu.Unlock()
246 tr := ac.transport()255 tr := ac.transport()
247- usedMu.Lock()256+ capture(tr)
248- used = tr
249- usedMu.Unlock()
250 if tr == nil {257 if tr == nil {
251 return errors.New("client transport is nil")258 return errors.New("client transport is nil")
252 }259 }
253 return tr.Send(ac.nextOutputFrameLocked(b))260 return tr.Send(ac.nextOutputFrameLocked(b))
254 })261 })
255- usedMu.Lock()
256- defer usedMu.Unlock()
257- return used, err
258 }262 }
259 263
260-func (d *Daemon) boundedSendWith(ac *attachedClient, send func() error) error {264+var errSendTimedOut = errors.New("send timed out")
265+
266+func (d *Daemon) boundedSendWith(ac *attachedClient, send func(capture func(ports.Transport)) error) (ports.Transport, error) {
261 timer := d.clock.NewTimer(detachNotifyTimeout)267 timer := d.clock.NewTimer(detachNotifyTimeout)
262 result := make(chan error, 1)268 result := make(chan error, 1)
269+ var (
270+ capturedMu sync.Mutex
271+ captured ports.Transport
272+ )
273+ capture := func(tr ports.Transport) {
274+ capturedMu.Lock()
275+ captured = tr
276+ capturedMu.Unlock()
277+ }
278+ capturedTransport := func() ports.Transport {
279+ capturedMu.Lock()
280+ defer capturedMu.Unlock()
281+ return captured
282+ }
263 go func() {283 go func() {
264- result <- send()284+ result <- send(capture)
265 }()285 }()
266 select {286 select {
267 case err := <-result:287 case err := <-result:
268 timer.Stop()288 timer.Stop()
269- return err289+ return capturedTransport(), err
270 case <-timer.C():290 case <-timer.C():
271 select {291 select {
272 case err := <-result:292 case err := <-result:
273- return err293+ return capturedTransport(), err
274 default:294 default:
275 }295 }
276- _ = ac.closeTransport()296+ return capturedTransport(), errSendTimedOut
277- return errors.New("send timed out")
278 }297 }
279 }298 }
280 299

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