Skip to content

Commit 25af70a

Browse files
drpcstream: wire the receive-window grant policy into the read path
Install a per-stream recvWindow and drive it from the read/recv path: - HandleFrame intercepts an incoming KindWindowUpdate before the assembler and applies it to the send window. Intercepting early keeps a grant that interleaves with an in-progress message (grants are emitted off the write lock) from disturbing reassembly. - A dispatched KindMessage frame returns credit via recvWindow, emitting a KindWindowUpdate once the accrued amount crosses the threshold and the high-water gate is open. - RawRecv/MsgRecv call recvWindow after dequeuing; consuming can reopen a gate the high-water mark had closed and flush the accrued grant. Grants are control frames, appended without blocking, so emitting them from the reader goroutine is safe. The window is opt-in: with no recvw installed no grants are emitted, and an incoming grant with no send window is ignored, so default behavior is unchanged. Stream-level only; the connection-level receive budget and the enablement path that installs the windows come in later commits. Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
1 parent 0551f9c commit 25af70a

2 files changed

Lines changed: 188 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright (C) 2026 Cockroach Labs.
2+
// See LICENSE for copying information.
3+
4+
package drpcstream
5+
6+
import (
7+
"context"
8+
"io"
9+
"testing"
10+
"time"
11+
12+
"github.com/zeebo/assert"
13+
14+
"storj.io/drpc/drpcwire"
15+
)
16+
17+
// captureWriter returns a MuxWriter whose output frames are decoded and
18+
// delivered on the returned channel, so tests can observe emitted grants.
19+
func captureWriter(t *testing.T) (*drpcwire.MuxWriter, <-chan drpcwire.Frame) {
20+
t.Helper()
21+
pr, pw := io.Pipe()
22+
mw := drpcwire.NewMuxWriter(pw, func(error) {})
23+
frames := make(chan drpcwire.Frame, 64)
24+
rd := drpcwire.NewReader(pr)
25+
go func() {
26+
for {
27+
fr, err := rd.ReadFrame()
28+
if err != nil {
29+
return
30+
}
31+
frames <- fr
32+
}
33+
}()
34+
t.Cleanup(func() {
35+
mw.Stop(nil)
36+
<-mw.Done()
37+
_ = pw.Close()
38+
_ = pr.Close()
39+
})
40+
return mw, frames
41+
}
42+
43+
func waitFrame(t *testing.T, frames <-chan drpcwire.Frame) drpcwire.Frame {
44+
t.Helper()
45+
select {
46+
case fr := <-frames:
47+
return fr
48+
case <-time.After(time.Second):
49+
t.Fatal("expected a frame to be written, got none")
50+
return drpcwire.Frame{}
51+
}
52+
}
53+
54+
func assertNoFrame(t *testing.T, frames <-chan drpcwire.Frame) {
55+
t.Helper()
56+
select {
57+
case fr := <-frames:
58+
t.Fatalf("unexpected frame written: %v", fr)
59+
case <-time.After(blockShort):
60+
}
61+
}
62+
63+
func msgFrame(sid, mid uint64, data []byte, done bool) drpcwire.Frame {
64+
return drpcwire.Frame{
65+
ID: drpcwire.ID{Stream: sid, Message: mid},
66+
Kind: drpcwire.KindMessage,
67+
Data: data,
68+
Done: done,
69+
}
70+
}
71+
72+
// An incoming KindWindowUpdate is intercepted and applied to the send window.
73+
func TestStream_IncomingGrantAppliesToSendWindow(t *testing.T) {
74+
st := newGateStream(t)
75+
st.sendw = newSendWindow(0)
76+
77+
assert.NoError(t, st.HandleFrame(drpcwire.WindowUpdateFrame(st.ID(), 500)))
78+
assert.Equal(t, st.sendw.available(), int64(500))
79+
}
80+
81+
// A grant interleaved with an in-progress message is intercepted before the
82+
// assembler, so reassembly is undisturbed.
83+
func TestStream_GrantDoesNotDisturbReassembly(t *testing.T) {
84+
st := newGateStream(t)
85+
sid := st.ID()
86+
87+
assert.NoError(t, st.HandleFrame(msgFrame(sid, 1, []byte("foo"), false)))
88+
assert.NoError(t, st.HandleFrame(drpcwire.WindowUpdateFrame(sid, 100))) // interleaved
89+
assert.NoError(t, st.HandleFrame(msgFrame(sid, 1, []byte("bar"), true)))
90+
91+
got, err := st.RawRecv()
92+
assert.NoError(t, err)
93+
assert.Equal(t, string(got), "foobar")
94+
}
95+
96+
// Dispatching a data frame returns credit once the threshold is met.
97+
func TestStream_DispatchEmitsGrant(t *testing.T) {
98+
mw, frames := captureWriter(t)
99+
st := NewWithOptions(context.Background(), 1, mw, NewBufferPool(), Options{SplitSize: 64 << 10})
100+
st.recvw = newRecvWindow(1<<20, 100)
101+
102+
assert.NoError(t, st.HandleFrame(msgFrame(st.ID(), 1, make([]byte, 150), true)))
103+
104+
fr := waitFrame(t, frames)
105+
sid, delta, ok := drpcwire.ParseWindowUpdate(fr)
106+
assert.That(t, ok)
107+
assert.Equal(t, sid, st.ID())
108+
assert.Equal(t, delta, uint64(150))
109+
}
110+
111+
// Above the high-water mark, dispatch withholds; consuming resumes and flushes
112+
// the accrued credit.
113+
func TestStream_ConsumeEmitsGrantOnResume(t *testing.T) {
114+
mw, frames := captureWriter(t)
115+
st := NewWithOptions(context.Background(), 1, mw, NewBufferPool(), Options{SplitSize: 64 << 10})
116+
st.recvw = newRecvWindow(100, 50)
117+
118+
// 120 bytes buffered >= high-water 100 -> withheld.
119+
assert.NoError(t, st.HandleFrame(msgFrame(st.ID(), 1, make([]byte, 120), true)))
120+
assertNoFrame(t, frames)
121+
122+
// Consume -> buffered drops below high-water -> flush accrued 120.
123+
_, err := st.RawRecv()
124+
assert.NoError(t, err)
125+
126+
fr := waitFrame(t, frames)
127+
_, delta, ok := drpcwire.ParseWindowUpdate(fr)
128+
assert.That(t, ok)
129+
assert.Equal(t, delta, uint64(120))
130+
}
131+
132+
// With no receive window installed, no grants are emitted and an incoming grant
133+
// with no send window is harmlessly ignored.
134+
func TestStream_NoWindowsNoGrants(t *testing.T) {
135+
mw, frames := captureWriter(t)
136+
st := NewWithOptions(context.Background(), 1, mw, NewBufferPool(), Options{SplitSize: 64 << 10})
137+
138+
assert.NoError(t, st.HandleFrame(msgFrame(st.ID(), 1, make([]byte, 150), true)))
139+
assertNoFrame(t, frames)
140+
141+
assert.NoError(t, st.HandleFrame(drpcwire.WindowUpdateFrame(st.ID(), 100)))
142+
}

drpcstream/stream.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ type Stream struct {
5959
// flow control is not enabled, in which case data writes are ungated.
6060
sendw *sendWindow
6161

62+
// recvw is the per-stream receive-side flow-control policy. It is nil when
63+
// flow control is not enabled, in which case no grants are emitted.
64+
recvw *recvWindow
65+
6266
mu sync.Mutex // protects state transitions
6367
sigs struct {
6468
send drpcsignal.Signal // set when done sending messages
@@ -211,10 +215,30 @@ func (s *Stream) HandleFrame(fr drpcwire.Frame) (err error) {
211215
return nil
212216
}
213217

218+
// Flow-control grants are out-of-band signaling, not part of the message
219+
// stream, so they are intercepted before the assembler. Grants are emitted
220+
// off the write lock, so one can arrive interleaved with an in-progress
221+
// message; intercepting here keeps it from disturbing reassembly. Without a
222+
// send window (flow control not enabled) the grant is simply ignored.
223+
if fr.Kind == drpcwire.KindWindowUpdate {
224+
if s.sendw != nil {
225+
if _, delta, ok := drpcwire.ParseWindowUpdate(fr); ok {
226+
s.sendw.grant(int64(delta))
227+
}
228+
}
229+
return nil
230+
}
231+
214232
packet, packetReady, err := s.pa.AppendFrame(fr)
215233
if err != nil {
216234
return err
217235
}
236+
237+
// A data frame dispatched off the wire may return credit to the sender.
238+
if fr.Kind == drpcwire.KindMessage && s.recvw != nil {
239+
s.emitGrant(s.recvw.dispatched(int64(len(fr.Data))))
240+
}
241+
218242
if !packetReady {
219243
return nil
220244
}
@@ -280,6 +304,18 @@ func (s *Stream) handlePacket(pkt drpcwire.Packet) (err error) {
280304
}
281305
}
282306

307+
// emitGrant sends a per-stream flow-control credit grant of delta bytes to the
308+
// peer, and is a no-op when delta is not positive. Grants are control frames,
309+
// which WriteFrame appends immediately without blocking on backpressure, so
310+
// this is safe to call from the reader goroutine. It is best-effort: a write
311+
// error means the stream is going away.
312+
func (s *Stream) emitGrant(delta int64) {
313+
if delta <= 0 {
314+
return
315+
}
316+
_ = s.wr.WriteFrame(drpcwire.WindowUpdateFrame(s.id.Stream, uint64(delta)), nil)
317+
}
318+
283319
//
284320
// helpers
285321
//
@@ -447,8 +483,13 @@ func (s *Stream) RawRecv() (data []byte, err error) {
447483
return nil, err
448484
}
449485
data = append([]byte(nil), b...)
486+
n := len(b)
450487
s.recvQueue.Done()
451488

489+
// Consuming buffered data can return credit to the sender.
490+
if s.recvw != nil {
491+
s.emitGrant(s.recvw.consumed(int64(n)))
492+
}
452493
return data, nil
453494
}
454495

@@ -488,9 +529,14 @@ func (s *Stream) MsgRecv(msg drpc.Message, enc drpc.Encoding) (err error) {
488529
if err != nil {
489530
return err
490531
}
532+
n := len(b)
491533
err = enc.Unmarshal(b, msg)
492534
s.recvQueue.Done()
493535

536+
// Consuming buffered data can return credit to the sender.
537+
if s.recvw != nil {
538+
s.emitGrant(s.recvw.consumed(int64(n)))
539+
}
494540
return err
495541
}
496542

0 commit comments

Comments
 (0)