Skip to content

Commit ff4c7f2

Browse files
drpcstream: install per-stream windows from a flow-control option
Add Options.FlowControl to enable per-stream flow control. When Enabled, NewWithOptions installs the send window (seeded with StreamWindow as initial credit) and the receive window (HighWater + GrantThreshold), so data writes become credit-gated and grants are emitted -- all driven by configuration rather than injected by tests. This is static, symmetric enablement: both peers must enable it with compatible sizes. Advertise-on-enable (safe under mixed enablement), the manager wiring that turns it on for a whole connection, and the connection-level window come in later commits. Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
1 parent e6a5378 commit ff4c7f2

2 files changed

Lines changed: 103 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (C) 2026 Cockroach Labs.
2+
// See LICENSE for copying information.
3+
4+
package drpcstream
5+
6+
import (
7+
"context"
8+
"testing"
9+
"time"
10+
11+
"github.com/zeebo/assert"
12+
13+
"storj.io/drpc/drpcwire"
14+
)
15+
16+
// Enabling flow control via Options installs the send and receive windows,
17+
// seeding the send window with the configured stream window as initial credit.
18+
func TestStream_FlowControlEnabledInstallsWindows(t *testing.T) {
19+
mw := testMuxWriter(t)
20+
st := NewWithOptions(context.Background(), 1, mw, NewBufferPool(), Options{
21+
SplitSize: 64 << 10,
22+
FlowControl: FlowControl{
23+
Enabled: true,
24+
StreamWindow: 256 << 10,
25+
HighWater: 4 << 20,
26+
GrantThreshold: 128 << 10,
27+
},
28+
})
29+
assert.That(t, st.sendw != nil)
30+
assert.That(t, st.recvw != nil)
31+
assert.Equal(t, st.sendw.available(), int64(256<<10))
32+
}
33+
34+
// Without flow control enabled, no windows are installed and behavior is
35+
// unchanged (ungated).
36+
func TestStream_FlowControlDisabledNoWindows(t *testing.T) {
37+
mw := testMuxWriter(t)
38+
st := NewWithOptions(context.Background(), 1, mw, NewBufferPool(), Options{SplitSize: 64 << 10})
39+
assert.That(t, st.sendw == nil)
40+
assert.That(t, st.recvw == nil)
41+
}
42+
43+
// End to end via the option: an option-installed window gates a data write that
44+
// exceeds the initial credit, and an incoming grant resumes it.
45+
func TestStream_FlowControlOptionGatesAndResumes(t *testing.T) {
46+
mw := testMuxWriter(t)
47+
st := NewWithOptions(context.Background(), 1, mw, NewBufferPool(), Options{
48+
SplitSize: 64 << 10,
49+
FlowControl: FlowControl{Enabled: true, StreamWindow: 4, HighWater: 1 << 20, GrantThreshold: 2},
50+
})
51+
52+
done := make(chan error, 1)
53+
go func() { done <- st.RawWrite(drpcwire.KindMessage, []byte("hello")) }() // 5 bytes > 4
54+
55+
select {
56+
case <-done:
57+
t.Fatal("write returned before sufficient credit")
58+
case <-time.After(blockShort):
59+
}
60+
61+
// A grant from the peer tops up the send window and resumes the write.
62+
assert.NoError(t, st.HandleFrame(drpcwire.WindowUpdateFrame(st.ID(), 1)))
63+
64+
select {
65+
case err := <-done:
66+
assert.NoError(t, err)
67+
case <-time.After(time.Second):
68+
t.Fatal("write did not resume after grant")
69+
}
70+
}

drpcstream/stream.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,35 @@ type Options struct {
3030
// more allocations. 0 is unlimited.
3131
MaximumBufferSize int
3232

33+
// FlowControl configures per-stream flow control. It is disabled by default.
34+
FlowControl FlowControl
35+
3336
// Internal contains options that are for internal use only.
3437
Internal drpcopts.Stream
3538
}
3639

40+
// FlowControl configures per-stream flow control. When Enabled, the stream
41+
// installs a send window (data writes acquire credit before going on the wire)
42+
// and a receive window (grants are returned as data is dispatched and
43+
// consumed). It is the caller's responsibility to size these so the liveness
44+
// rule StreamWindow >= 2*SplitSize holds.
45+
type FlowControl struct {
46+
// Enabled turns per-stream flow control on for the stream.
47+
Enabled bool
48+
49+
// StreamWindow is the per-stream send window: the initial send credit and
50+
// the nominal window size.
51+
StreamWindow int64
52+
53+
// HighWater is the receive-side buffered-byte high-water mark above which
54+
// grants are withheld.
55+
HighWater int64
56+
57+
// GrantThreshold is the amount of returnable credit that must accrue before
58+
// a grant is emitted, coalescing many frames into one KindWindowUpdate.
59+
GrantThreshold int64
60+
}
61+
3762
// Stream represents an rpc actively happening on a transport.
3863
type Stream struct {
3964
ctx streamCtx
@@ -123,6 +148,14 @@ func NewWithOptions(
123148

124149
s.recvQueue.init(pool)
125150

151+
// When flow control is enabled, install the per-stream windows. The send
152+
// window starts with StreamWindow credit (statically agreed with the peer);
153+
// the receive window drives grant emission.
154+
if opts.FlowControl.Enabled {
155+
s.sendw = newSendWindow(opts.FlowControl.StreamWindow)
156+
s.recvw = newRecvWindow(opts.FlowControl.HighWater, opts.FlowControl.GrantThreshold)
157+
}
158+
126159
return s
127160
}
128161

0 commit comments

Comments
 (0)