drpcstream,drpcmanager: enable per-stream flow control (grant-on-consume) - #89
Conversation
b7f5aee to
eaa5f1e
Compare
eaa5f1e to
88c548d
Compare
88c548d to
63ac053
Compare
shubhamdhama
left a comment
There was a problem hiding this comment.
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.
| func (s *Stream) installFlowControl() { | ||
| fc := drpcopts.GetStreamFlowControl(&s.opts.Internal) | ||
| if !fc.Enabled { | ||
| return | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
In fact, I would prefer if we eventually remove the internal/drpcopts package altogether.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Got it - we can reconsider during enablement.
Yes, that is implemented in the next stacked PR - #92.
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. |
| // 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. |
There was a problem hiding this comment.
This deserves a comment with an example of what would happen if we don't hold this condition true.
There was a problem hiding this comment.
done - added an example in the PR description and also a comment.
| func (s *Stream) installFlowControl() { | ||
| fc := drpcopts.GetStreamFlowControl(&s.opts.Internal) | ||
| if !fc.Enabled { | ||
| return | ||
| } |
There was a problem hiding this comment.
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.
63ac053 to
64b9b96
Compare
…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>
64b9b96 to
c2daf8c
Compare
Installs the per-stream windows when
FlowControlis set: the send window seeded withStreamWindowcredit and watching the send signal for termination; the receive window returning consumed-byte credit coalesced atGrantThreshold.Enablement is internal-only (merge safety). The option lives in
internal/drpcopts(SetStreamFlowControl), not on the publicdrpcstream.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:
StreamWindowandGrantThresholdmust be positive;SplitSizeusesSplitData's 64 KiB default as the frame size;GrantThreshold + frame ≤ StreamWindow— the liveness guarantee for grant coalescing. WithG = GrantThreshold,W = StreamWindow,F = frame: once the receive side has consumed more thanW − Fof the window, the sender has< Fcredit left and halts; ifG > W − F, the receiver cannot emit a grant to unblock it either — deadlock. RequiringG ≤ W − Fprevents that.The liveness condition (enforced by
validateFlowControlindrpcstream/stream.go) is: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
Gbytes, then sends one window update. But the sender can only haveWbytes in flight, and it stalls the moment it can't afford the next frame — so it can be left holding up toFof credit it can't spend. That means only aboutW − Fis guaranteed to be delivered-and-consumable before the sender is stuck. If the coalescing thresholdGis 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 setsG = 80 KiB.Check:
G ≤ W − F→80 ≤ 32? No — violated.96 → 32.< G(80 KiB) → withholds the grant.Example that's healthy (condition satisfied)
Same
W = 96 KiB,F = 64 KiB, butG = 32 KiB→32 ≤ 32✓.96 → 32; tries message 2, blocks.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.