Skip to content

drpcstream: grant-on-consume receive-side flow control - #88

Merged
suj-krishnan merged 1 commit into
mainfrom
sujatha/flow-control-recv-window-consume
Jul 29, 2026
Merged

drpcstream: grant-on-consume receive-side flow control#88
suj-krishnan merged 1 commit into
mainfrom
sujatha/flow-control-recv-window-consume

Conversation

@suj-krishnan

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

Copy link
Copy Markdown

Adds the receive side of per-stream flow control: credit is returned to the sender as the application consumes received bytes (grant-on-consume), coalesced into one window update per threshold bytes.

This approach was picked over a grant-on-dispatch design (credit returned on receipt, gated by a high-water mark over buffered bytes — see #72) for its simplicity and tight memory bound: in-flight plus buffered bytes never exceed
the window, and the receive side needs almost no machinery.

What's here

  • recvWindow is a consumed-bytes counter and a threshold:RawRecv/MsgRecv accrue the dequeued wire bytes on the way out (deferred, so a decode failure still releases them) and emit a KindWindowUpdate once the accrued credit reaches the threshold, bounding update frequency to one per threshold bytes. Withheld credit is at most threshold bytes and flushes with the next grant — never lost.
  • No mutex, no buffered/dispatched tracking — the read path is already serialized by the read lock, and grant-on-consume never grants on receipt.
  • HandleFrame gains only the KindWindowUpdate intercept: an incoming grant is applied to the send window before the assembler, so a grant interleaved with an in-progress message cannot disturb reassembly.

Deliberate trade-offs

  • The threshold must be small relative to the sender's window, or the withheld credit can stall the sender; the enablement path (next PR) will validate this. (grpc-go replenishes at limit/4, hashicorp/yamux at max/2.)

@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.

I also think sending grants for ever single consumption would be very chatty. We should send at fixed thresholds. I liked what Yamux have done here: https://github.com/hashicorp/yamux/blob/76348a0c5ab413fe3c5c564077ea26e21a8769a3/stream.go#L278

@suj-krishnan

Copy link
Copy Markdown
Author

I also think sending grants for ever single consumption would be very chatty. We should send at fixed thresholds. I liked what Yamux have done here: https://github.com/hashicorp/yamux/blob/76348a0c5ab413fe3c5c564077ea26e21a8769a3/stream.go#L278

Agreed - wanted to take it up as a follow-up but it is a fairly simple change (the previous PR already had similar accounting).

@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-recv-window-consume branch from 8b5880b to 9726a8d Compare July 28, 2026 10:37
@suj-krishnan

Copy link
Copy Markdown
Author

I also think sending grants for ever single consumption would be very chatty. We should send at fixed thresholds. I liked what Yamux have done here: https://github.com/hashicorp/yamux/blob/76348a0c5ab413fe3c5c564077ea26e21a8769a3/stream.go#L278

Agreed - wanted to take it up as a follow-up but it is a fairly simple change (the previous PR already had similar accounting).

The PR is updated. The 'threshold' will be a fraction of the stream window (either 1/4 or 1/2).

@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.

LGTM!

Comment thread drpcstream/recv_window.go Outdated
Comment on lines +10 to +11
// It holds no lock: consumed is called only from the stream read path
// (RawRecv/MsgRecv), which the read lock already serializes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We don't need to say "It holds no lock...". Agents tend to over-comment.

Comment thread drpcstream/recv_window_wiring_test.go Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Please merge these tests within the recv_window_test.go. It's an unnecessary addition of files.

Comment thread drpcstream/stream.go Outdated
Comment on lines +68 to +72
// recvw is the per-stream receive-side flow-control window. The read path
// returns credit to the sender through it as messages are consumed,
// coalesced at its threshold. It is nil when flow control is not enabled,
// in which case no grants are emitted.
recvw *recvWindow

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: over-commenting. It's leaking the implementation details here. This commenting is already done in the definition of recvWindow.

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.

Yeah, it keeps re-adding comments during subsequent commits even after I remove them 😢 So I change the comments just once before I push.

Comment thread drpcstream/stream.go Outdated
@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-recv-window-consume branch 3 times, most recently from 7f8e018 to 12ae0a8 Compare July 29, 2026 08:42
Add the receive side of per-stream flow control: return credit to the
sender as the application consumes received bytes (grant-on-consume),
coalesced into one KindWindowUpdate per threshold bytes.

Chosen over a grant-on-dispatch design (a high-water gate over buffered
bytes, prototyped in an earlier revision of this work) for its simplicity
and its tight memory bound: in-flight plus buffered bytes never exceed
the window. The receive side needs almost no machinery:

  - recvWindow is a consumed-byte counter and a threshold; consumed
    returns a coalesced grant once the accrued amount crosses the
    threshold. No mutex -- consumed is called only from the read path,
    which the read lock already serializes -- and no buffered/dispatched
    tracking, since grant-on-consume never grants on receipt.
  - RawRecv/MsgRecv return credit for the consumed wire bytes inline,
    before decompression reassigns the buffer and before any decode error
    can return, so credit is released on every path without a defer.
  - HandleFrame gains only the KindWindowUpdate intercept: an incoming
    grant is applied to the send window before the assembler, so a grant
    interleaved with an in-progress message cannot disturb reassembly.
  - No discarded-partial accounting. A partial superseded by a higher
    message id only happens when the sender aborted mid-message, and
    every such path terminates the send side, so the unreturned credit
    belongs to a stream that is already done; there is no receiver-side
    counter for it to corrupt. Revisit if a connection-level receive
    budget aggregates credit across streams.

emitGrant is a no-op once the stream has terminated: the ring drains
queued messages after close, and returning credit behind the terminal
frame would only invite more doomed data.

Trade-offs, both deliberate: throughput is capped at window/RTT (no BDP
estimation), and coalescing withholds at most threshold bytes of credit,
which the enablement path keeps small relative to the window. A larger
threshold cuts control-frame chatter at the cost of more withheld credit.

Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
@suj-krishnan
suj-krishnan force-pushed the sujatha/flow-control-recv-window-consume branch from 12ae0a8 to 5a99ec2 Compare July 29, 2026 08:47
@suj-krishnan
suj-krishnan merged commit 25a6979 into main Jul 29, 2026
3 checks passed
@suj-krishnan
suj-krishnan deleted the sujatha/flow-control-recv-window-consume branch July 29, 2026 08:49
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