fix(remote): preserve transport identity on sends
- commit
- 56875c746291d8c62772bace3d07d70527d0e01c
- author
- brice · · 2026-07-04 12:55 UTC
- parent
- 9da1afc
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 288 | 288 | return | |
| 289 | 289 | } | |
| 290 | 290 | if reliable { | |
| 291 | - | t.sendAck(seq) | |
| 292 | - | t.enqueueReliable(seq, f) | |
| 291 | + | if t.enqueueReliable(seq, f) { | |
| 292 | + | t.sendAck(seq) | |
| 293 | + | } | |
| 293 | 294 | return | |
| 294 | 295 | } | |
| 295 | 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 | 301 | t.deliverMu.Lock() | |
| 301 | 302 | defer t.deliverMu.Unlock() | |
| 302 | 303 | if seq < t.nextRecvSeq { | |
| 303 | - | return | |
| 304 | + | return true | |
| 304 | 305 | } | |
| 305 | 306 | if _, exists := t.recvBuf[seq]; exists { | |
| 306 | - | return | |
| 307 | + | return true | |
| 307 | 308 | } | |
| 308 | 309 | if len(t.recvBuf) >= maxRecvBuffer { | |
| 309 | 310 | if seq != t.nextRecvSeq { | |
| 310 | - | return | |
| 311 | + | return false | |
| 311 | 312 | } | |
| 312 | 313 | for bufferedSeq := range t.recvBuf { | |
| 313 | 314 | delete(t.recvBuf, bufferedSeq) | |
| @@ -316,6 +317,7 @@ func (t *Transport) enqueueReliable(seq uint64, f ports.Frame) { | |||
| 316 | 317 | } | |
| 317 | 318 | t.recvBuf[seq] = f | |
| 318 | 319 | t.deliverCond.Signal() | |
| 320 | + | return true | |
| 319 | 321 | } | |
| 320 | 322 | ||
| 321 | 323 | func (t *Transport) deliveryLoop() { | |
| @@ -288,26 +288,27 @@ func (t *Transport) handleRecord(p []byte) { | |||||
| 288 | return | 288 | 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 | return | 294 | 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 | - | return | 304 | + | return true |
| 304 | } | 305 | } | ||
| 305 | if _, exists := t.recvBuf[seq]; exists { | 306 | if _, exists := t.recvBuf[seq]; exists { | ||
| 306 | - | return | 307 | + | 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 | - | return | 311 | + | 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] = f | 318 | 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) { | |||
| 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 | 369 | func recvMaybe(tr *Transport, d time.Duration) (ports.Frame, bool) { | |
| 341 | 370 | ch := make(chan ports.Frame, 1) | |
| 342 | 371 | 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 { | |||
| 208 | 208 | ||
| 209 | 209 | // send serialises a frame onto the client's transport. | |
| 210 | 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 | 216 | ac.sendMu.Lock() | |
| 212 | 217 | defer ac.sendMu.Unlock() | |
| 213 | 218 | tr := ac.transport() | |
| 214 | 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 | 225 | // 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 | 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 | 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 | 251 | 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 { | |
| 244 | 253 | ac.sendMu.Lock() | |
| 245 | 254 | defer ac.sendMu.Unlock() | |
| 246 | 255 | tr := ac.transport() | |
| 247 | - | usedMu.Lock() | |
| 248 | - | used = tr | |
| 249 | - | usedMu.Unlock() | |
| 256 | + | capture(tr) | |
| 250 | 257 | if tr == nil { | |
| 251 | 258 | return errors.New("client transport is nil") | |
| 252 | 259 | } | |
| 253 | 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 | 267 | timer := d.clock.NewTimer(detachNotifyTimeout) | |
| 262 | 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 | 283 | go func() { | |
| 264 | - | result <- send() | |
| 284 | + | result <- send(capture) | |
| 265 | 285 | }() | |
| 266 | 286 | select { | |
| 267 | 287 | case err := <-result: | |
| 268 | 288 | timer.Stop() | |
| 269 | - | return err | |
| 289 | + | return capturedTransport(), err | |
| 270 | 290 | case <-timer.C(): | |
| 271 | 291 | select { | |
| 272 | 292 | case err := <-result: | |
| 273 | - | return err | |
| 293 | + | return capturedTransport(), err | |
| 274 | 294 | default: | |
| 275 | 295 | } | |
| 276 | - | _ = ac.closeTransport() | |
| 277 | - | return errors.New("send timed out") | |
| 296 | + | return capturedTransport(), errSendTimedOut | |
| 278 | 297 | } | |
| 279 | 298 | } | |
| 280 | 299 | ||
| @@ -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 (including | 225 | // 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 err | 289 | + | 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 err | 293 | + | 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 | ||||