Skip to content

drpcstream,drpcmanager: enable per-stream flow control (grant-on-consume) - #89

Merged
suj-krishnan merged 1 commit into
mainfrom
sujatha/flow-control-consume-enable
Aug 4, 2026
Merged

drpcstream,drpcmanager: enable per-stream flow control (grant-on-consume)#89
suj-krishnan merged 1 commit into
mainfrom
sujatha/flow-control-consume-enable

Conversation

@suj-krishnan

@suj-krishnan suj-krishnan commented Jul 28, 2026

Copy link
Copy Markdown

Installs the per-stream windows when FlowControl is set: the send window seeded with StreamWindow credit and watching the send signal for termination; the receive window returning consumed-byte credit coalesced at GrantThreshold.

Enablement is internal-only (merge safety). The option lives in internal/drpcopts (SetStreamFlowControl), not on the public drpcstream.Options — so a consumer bumping the drpc dependency mid-implementation cannot enable flow control: no public API reaches it. Promotion to a public option is a later, deliberate change.

Validation at the installation site. An invalid configuration resorts to mutually-consistent defaults (and logs the override) instead of failing the stream:

  • StreamWindow and GrantThreshold must be positive;
  • a non-positive SplitSize uses SplitData's 64 KiB default as the frame size;
  • GrantThreshold + frame ≤ StreamWindow — the liveness guarantee for grant coalescing. With G = GrantThreshold, W = StreamWindow, F = frame: once the receive side has consumed more than W − F of the window, the sender has < F credit left and halts; if G > W − F, the receiver cannot emit a grant to unblock it either — deadlock. Requiring G ≤ W − F prevents that.

The liveness condition (enforced by validateFlowControl in drpcstream/stream.go) is:

GrantThreshold ≤ StreamWindow − frame (equivalently, G + F ≤ W)

where G = GrantThreshold, W = StreamWindow, F = the per-frame size (SplitSize).

Why it exists

Grant coalescing means the receiver withholds credit until the app has consumed at least G bytes, then sends one window update. But the sender can only have W bytes in flight, and it stalls the moment it can't afford the next frame — so it can be left holding up to F of credit it can't spend. That means only about W − F is guaranteed to be delivered-and-consumable before the sender is stuck. If the coalescing threshold G is bigger than that, the receiver may never accumulate enough consumed bytes to fire a grant — and if it never grants, the stalled sender never wakes. Deadlock.

Example that deadlocks (condition violated)

Say W = 96 KiB, F = 64 KiB (one 64 KiB message per frame), and someone sets G = 80 KiB.
Check: G ≤ W − F80 ≤ 32? No — violated.

  1. Sender sends message 1 (64 KiB): credit 96 → 32.
  2. Sender wants to send message 2 (needs 64 KiB) but only has 32 KiB credit → blocks.
  3. Receiver consumes message 1 → accrued consumed = 64 KiB, which is < G (80 KiB) → withholds the grant.
  4. Nothing more has arrived to consume, and the sender can't send more without a grant → both sides stuck forever.

Example that's healthy (condition satisfied)

Same W = 96 KiB, F = 64 KiB, but G = 32 KiB32 ≤ 32 ✓.

  1. Sender sends message 1 (64 KiB): credit 96 → 32; tries message 2, blocks.
  2. Receiver consumes message 1 → accrued 64 KiB ≥ G (32 KiB) → grants 64 KiB → sender credit back to 96 → it sends the next message. Progress.

The defaults that we set have huge margin:
W = 2 MiB, F = 64 KiB, G = 512 KiB:
G + F = 576 KiB ≤ 2 MiB ✓ — satisfied with room to spare.

Base automatically changed from sujatha/flow-control-recv-window-consume to main July 29, 2026 08:49
@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-consume-enable branch 5 times, most recently from b7f5aee to eaa5f1e Compare July 29, 2026 14:56
@suj-krishnan
suj-krishnan marked this pull request as draft July 30, 2026 02:21
@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-consume-enable branch from eaa5f1e to 88c548d Compare July 30, 2026 04:10
@suj-krishnan
suj-krishnan marked this pull request as ready for review August 3, 2026 05:19
@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-consume-enable branch from 88c548d to 63ac053 Compare August 3, 2026 05:34

@shubhamdhama shubhamdhama left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I want to clarify before we move forward. We decided to keep things simpler on the receive window side by sending ack only after we have a complete packet. That means, if we have a 1MiB packet and a send window of 256KiB, the sender will be deadlocked because receiver won't send any update until it receive the complete packet.

One cure to this was the suggestion that flow control should be applied at the packet level. If we have a message and hence a packet of 1MiB size, we send that whole packet to the other side and make the available grant negative (256KiB-1MiB). This loosens up the guarantees that flow control gives us around the memory but I doubt that most of the requests are this big so this simple model may just work fine with cockroach.

Comment thread drpcstream/stream.go
Comment on lines +154 to +158
func (s *Stream) installFlowControl() {
fc := drpcopts.GetStreamFlowControl(&s.opts.Internal)
if !fc.Enabled {
return
}

@shubhamdhama shubhamdhama Aug 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an unnecessary ceremony to keep FlowControl configuration in internal/drpcopts/stream.go. We can have that configuration right here in the stream. It's adding unnecessary indirection. I think the code will be more readable if that's in this package and even this stream file itself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I would prefer if we eventually remove the internal/drpcopts package altogether.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is temporary, so we can write end-end tests until we are integration ready (see PR description/comments). These options will eventually reside directly in drpcstream/stream.go. Integration with cockroach/enablement is tracked separately.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore if you've already removed this in a stacked PR. If not, I would still recommend to fix what I've said above. I'm not saying move this to the public Options. I'm saying, can we not just move FlowControl object here. I will leave this up to you.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it - we can reconsider during enablement.

@suj-krishnan

suj-krishnan commented Aug 4, 2026

Copy link
Copy Markdown
Author

One thing I want to clarify before we move forward. We decided to keep things simpler on the receive window side by sending ack only after we have a complete packet. That means, if we have a 1MiB packet and a send window of 256KiB, the sender will be deadlocked because receiver won't send any update until it receive the complete packet.

One cure to this was the suggestion that flow control should be applied at the packet level. If we have a message and hence a packet of 1MiB size, we send that whole packet to the other side and make the available grant negative (256KiB-1MiB). This loosens up the guarantees that flow control gives us around the memory but I doubt that most of the requests are this big so this simple model may just work fine with cockroach.

Yes, that is implemented in the next stacked PR - #92.

but I doubt that most of the requests are this big

This is not true unfortunately - we do have requests/responses that can carry significant payload and we may need to figure just how much of a blocker this is. I'm working on ascertaining the worst case size for certain APIs.

Comment thread drpcstream/stream.go
Comment on lines +42 to +44
// validateFlowControl reports whether a flow-control configuration can make
// progress: the window must be able to hold one frame plus the coalescing
// reserve (GrantThreshold). splitSize is the bounded (positive) per-frame size.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deserves a comment with an example of what would happen if we don't hold this condition true.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done - added an example in the PR description and also a comment.

Comment thread drpcstream/stream.go
Comment on lines +154 to +158
func (s *Stream) installFlowControl() {
fc := drpcopts.GetStreamFlowControl(&s.opts.Internal)
if !fc.Enabled {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore if you've already removed this in a stacked PR. If not, I would still recommend to fix what I've said above. I'm not saying move this to the public Options. I'm saying, can we not just move FlowControl object here. I will leave this up to you.

@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-consume-enable branch from 63ac053 to 64b9b96 Compare August 4, 2026 15:58
…option

Add drpcopts.FlowControl (Enabled, StreamWindow, GrantThreshold) and
install the per-stream windows at stream creation when it is set: the
send window seeded with StreamWindow credit and watching the send signal
for termination, and the receive window returning consumed-byte credit
coalesced at GrantThreshold.

Enablement is internal-only for merge safety: the option lives in
internal/drpcopts, not on the public drpcstream.Options, so a consumer
bumping the dependency cannot enable flow control before the deliberate
promotion to a public option.

The installation site normalizes the configuration rather than failing.
The frame size follows SplitData: zero uses the 64 KiB default, and a
negative SplitSize means unbounded frames, which carry no per-frame
bound. A valid config needs positive sizes and, for bounded frames,
GrantThreshold plus one frame fitting in StreamWindow -- so credit
withheld by coalescing cannot strand the sender below the next frame's
cost. Anything that cannot make progress resorts to
FlowControl.SetDefaults and logs the override, so a misconfiguration
degrades to a working stream instead of crashing a process that embeds
drpc.

Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-consume-enable branch from 64b9b96 to c2daf8c Compare August 4, 2026 16:01
@suj-krishnan
suj-krishnan merged commit 628d079 into main Aug 4, 2026
3 checks passed
@suj-krishnan
suj-krishnan deleted the sujatha/flow-control-consume-enable branch August 4, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants