detached c3dcda7

fix(dgram): route acknowledgements through control path

commit
c3dcda7f49508efa5663e00254465f0fc1ae17ef
author
brice · · 2026-07-15 04:56 UTC
parent
ad8c50b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +39 -35
M internal/adapters/dgram/control.go +1 -35
@@ -216,39 +216,5 @@ func (t *Transport) sendControl(kind byte, id uint64) error {
216216 }
217217
218218 func (t *Transport) sendAck(seq uint64) {
219- var b [9]byte
220- b[0] = recAck
221- binary.BigEndian.PutUint64(b[1:], seq)
222- _ = t.sendPayload(b[:])
223-}
224-func (t *Transport) sendPayload(p []byte) error {
225- frags, err := pdgram.FragmentPayload(t.nextCounter(), p, t.mtu-pdgram.HeaderSize-t.codec.Overhead())
226- if err != nil {
227- return err
228- }
229- for _, f := range frags {
230- raw, err := pdgram.MarshalFragment(f)
231- if err != nil {
232- return err
233- }
234- pkt := t.codec.Seal(t.sendDir, t.nextCounter(), raw, nil)
235- t.mu.Lock()
236- peer := t.peer
237- closed := t.closed
238- closeErr := t.closeErr
239- t.mu.Unlock()
240- if closed {
241- if closeErr != nil {
242- return closeErr
243- }
244- return errors.New("dgram: closed")
245- }
246- if peer == nil {
247- return errors.New("dgram: no peer")
248- }
249- if err := t.writePacket(pkt, peer); err != nil {
250- return err
251- }
252- }
253- return nil
219+ _ = t.sendControl(recAck, seq)
254220 }
@@ -216,39 +216,5 @@ func (t *Transport) sendControl(kind byte, id uint64) error {
216 }216 }
217 217
218 func (t *Transport) sendAck(seq uint64) {218 func (t *Transport) sendAck(seq uint64) {
219- var b [9]byte219+ _ = t.sendControl(recAck, seq)
220- b[0] = recAck
221- binary.BigEndian.PutUint64(b[1:], seq)
222- _ = t.sendPayload(b[:])
223-}
224-func (t *Transport) sendPayload(p []byte) error {
225- frags, err := pdgram.FragmentPayload(t.nextCounter(), p, t.mtu-pdgram.HeaderSize-t.codec.Overhead())
226- if err != nil {
227- return err
228- }
229- for _, f := range frags {
230- raw, err := pdgram.MarshalFragment(f)
231- if err != nil {
232- return err
233- }
234- pkt := t.codec.Seal(t.sendDir, t.nextCounter(), raw, nil)
235- t.mu.Lock()
236- peer := t.peer
237- closed := t.closed
238- closeErr := t.closeErr
239- t.mu.Unlock()
240- if closed {
241- if closeErr != nil {
242- return closeErr
243- }
244- return errors.New("dgram: closed")
245- }
246- if peer == nil {
247- return errors.New("dgram: no peer")
248- }
249- if err := t.writePacket(pkt, peer); err != nil {
250- return err
251- }
252- }
253- return nil
254 }220 }
M internal/adapters/dgram/control_plane_test.go +38 -0
@@ -208,6 +208,44 @@ func TestACKDeadlineDispatchesWhileAdvisoryWriteIsBlocked(t *testing.T) {
208208 awaitSignal(t, ackStarted, "ACK write while advisory probe remains blocked")
209209 }
210210
211+func TestACKBypassesDataWriterAndRemainsDecodable(t *testing.T) {
212+ aPC, bPC := newPair()
213+ a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, Options{
214+ ResendAfter: time.Hour, Heartbeat: time.Hour,
215+ })
216+ if err != nil {
217+ t.Fatal(err)
218+ }
219+ defer func() { _ = a.Close() }()
220+
221+ acks := make(chan uint64, 1)
222+ aPC.drop = func(pkt []byte, _ net.Addr) bool {
223+ _, raw, err := a.codec.Open(pkt, a.sendDir, nil, nil)
224+ if err != nil {
225+ t.Errorf("open ACK: %v", err)
226+ return true
227+ }
228+ frag, err := pdgram.UnmarshalFragment(raw)
229+ if err != nil {
230+ t.Errorf("unmarshal ACK: %v", err)
231+ return true
232+ }
233+ if frag.Count != 1 || len(frag.Data) != 9 || frag.Data[0] != recAck {
234+ t.Errorf("control record=%x, want encoded ACK", frag.Data)
235+ return true
236+ }
237+ acks <- binary.BigEndian.Uint64(frag.Data[1:])
238+ return true
239+ }
240+
241+ a.writeMu.Lock()
242+ defer a.writeMu.Unlock()
243+ go a.sendAck(42)
244+ if seq := awaitResult(t, acks, "ACK while data writer is blocked"); seq != 42 {
245+ t.Fatalf("ACK sequence=%d, want 42", seq)
246+ }
247+}
248+
211249 func TestProbeDoesNotBlockOnDataWriter(t *testing.T) {
212250 aPC, bPC := newPair()
213251 a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, Options{
@@ -208,6 +208,44 @@ func TestACKDeadlineDispatchesWhileAdvisoryWriteIsBlocked(t *testing.T) {
208 awaitSignal(t, ackStarted, "ACK write while advisory probe remains blocked")208 awaitSignal(t, ackStarted, "ACK write while advisory probe remains blocked")
209 }209 }
210 210
211+func TestACKBypassesDataWriterAndRemainsDecodable(t *testing.T) {
212+ aPC, bPC := newPair()
213+ a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, Options{
214+ ResendAfter: time.Hour, Heartbeat: time.Hour,
215+ })
216+ if err != nil {
217+ t.Fatal(err)
218+ }
219+ defer func() { _ = a.Close() }()
220+
221+ acks := make(chan uint64, 1)
222+ aPC.drop = func(pkt []byte, _ net.Addr) bool {
223+ _, raw, err := a.codec.Open(pkt, a.sendDir, nil, nil)
224+ if err != nil {
225+ t.Errorf("open ACK: %v", err)
226+ return true
227+ }
228+ frag, err := pdgram.UnmarshalFragment(raw)
229+ if err != nil {
230+ t.Errorf("unmarshal ACK: %v", err)
231+ return true
232+ }
233+ if frag.Count != 1 || len(frag.Data) != 9 || frag.Data[0] != recAck {
234+ t.Errorf("control record=%x, want encoded ACK", frag.Data)
235+ return true
236+ }
237+ acks <- binary.BigEndian.Uint64(frag.Data[1:])
238+ return true
239+ }
240+
241+ a.writeMu.Lock()
242+ defer a.writeMu.Unlock()
243+ go a.sendAck(42)
244+ if seq := awaitResult(t, acks, "ACK while data writer is blocked"); seq != 42 {
245+ t.Fatalf("ACK sequence=%d, want 42", seq)
246+ }
247+}
248+
211 func TestProbeDoesNotBlockOnDataWriter(t *testing.T) {249 func TestProbeDoesNotBlockOnDataWriter(t *testing.T) {
212 aPC, bPC := newPair()250 aPC, bPC := newPair()
213 a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, Options{251 a, err := NewTransportWithOptions(aPC, bPC.addr, key(), 1, 2, Options{

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