Flow Control in Quill¶
Overview¶
Quill implements credit-based flow control to prevent buffer overflow in streaming RPCs. This ensures that fast senders don't overwhelm slow receivers.
Architecture¶
Credit Frames¶
Credit frames (flag bit 3) are used to grant permission to send messages. The payload of a credit frame contains a varint-encoded u32 representing the number of credits being granted.
// Create a credit frame granting 16 credits
let frame = Frame::credit(16);
// Decode credits from a frame
if let Some(credits) = frame.decode_credit() {
println!("Received {} credits", credits);
}
Credit Tracker¶
The CreditTracker provides thread-safe credit tracking using atomic operations:
use quill_core::CreditTracker;
// Create tracker with default credits (16)
let tracker = CreditTracker::with_defaults();
// Try to consume a credit before sending
if tracker.try_consume() {
// Send message
println!("Sent message");
} else {
println!("No credits available, waiting...");
}
// Grant more credits (from receiver)
tracker.grant(8);
Flow Control in Different Streaming Modes¶
Server Streaming¶
In server streaming, the client receives a stream of messages from the server:
- Server (sender) tracks send credits
- Client (receiver) grants credits by sending CREDIT frames
- Client grants initial credits when starting the stream
- Client grants more credits as it consumes messages
Current Implementation: The client tracks received messages and logs when it would grant credits. In a future HTTP/2 implementation, the client will send actual CREDIT frames back to the server.
Client Streaming¶
In client streaming, the server receives a stream of messages from the client:
- Client (sender) tracks send credits
- Server (receiver) grants credits by sending CREDIT frames
- Server grants initial credits when accepting the stream
- Server grants more credits as it consumes messages
Current Implementation: The server tracks received messages and logs when it would grant credits. The current implementation buffers all client messages before sending, so credits aren't enforced yet.
Bidirectional Streaming¶
In bidirectional streaming, both sides act as sender and receiver:
- Each side maintains two
CreditTrackerinstances: - One for tracking credits granted to us (for sending)
- One for tracking credits we've granted to the peer (for receiving)
- Both sides send and receive CREDIT frames independently
Current Implementation: Both client and server track credits and handle CREDIT frames. The request stream is currently buffered, but the response stream handles credits properly.
Configuration¶
Default configuration constants:
// Initial credits granted to senders
pub const DEFAULT_INITIAL_CREDITS: u32 = 16;
// Credits to grant when buffer space becomes available
pub const DEFAULT_CREDIT_REFILL: u32 = 8;
Frame Protocol¶
Frame Format¶
All frames follow this format:
Flags¶
DATA(bit 0): Frame contains dataEND_STREAM(bit 1): Stream has endedCANCEL(bit 2): Stream was cancelledCREDIT(bit 3): Frame contains credit grant
Credit Frame Example¶
A credit frame granting 100 credits:
Implementation Status¶
✅ Completed¶
- Credit frame format
-
CreditTrackerwith atomic operations - Credit handling in
ResponseFrameStream(client receives) - Credit handling in
RequestFrameStream(server receives) - Credit frame tests
- Documentation
🚧 Future Work¶
- Actual credit frame transmission over HTTP/2
- Dynamic credit adjustment based on buffer size
- Credit-based backpressure in client request streaming
- Configurable credit windows per RPC method
- Credit exhaustion metrics and monitoring
Testing¶
Credit tracking is tested in crates/quill-core/src/flow_control.rs:
#[test]
fn test_credit_consumption() {
let tracker = CreditTracker::new(5);
assert!(tracker.try_consume()); // 4 remaining
assert!(tracker.try_consume()); // 3 remaining
assert_eq!(tracker.available(), 3);
}
#[test]
fn test_credit_exhaustion() {
let tracker = CreditTracker::new(2);
assert!(tracker.try_consume()); // 1 remaining
assert!(tracker.try_consume()); // 0 remaining
assert!(!tracker.try_consume()); // Should fail
}
Credit frame encoding/decoding is tested in crates/quill-core/src/framing.rs:
#[test]
fn test_credit_frame_roundtrip() {
let original = Frame::credit(100);
let encoded = original.encode();
let mut parser = FrameParser::new();
parser.feed(&encoded);
let decoded = parser.parse_frame().unwrap().unwrap();
assert_eq!(decoded.decode_credit(), Some(100));
}
HTTP Transport Considerations¶
HTTP/1.1¶
HTTP/1.1 doesn't support true bidirectional streaming. The current implementation: - Buffers client request streams before sending - Uses chunked transfer encoding for server response streams - Cannot send credit frames from client to server
HTTP/2¶
HTTP/2 provides true bidirectional streaming with multiplexing: - Client can send credit frames while receiving response stream - Server can send credit frames while receiving request stream - Credit frames can flow independently in both directions
HTTP/3 (QUIC)¶
HTTP/3 over QUIC provides additional benefits: - Native stream multiplexing - Stream-level flow control at transport layer - Application-level credit control on top of QUIC flow control
Example Usage¶
use quill_client::QuillClient;
use bytes::Bytes;
// Server streaming with flow control
let client = QuillClient::new("http://localhost:8080");
let request = Bytes::from("request data");
let mut stream = client
.call_server_streaming("log.v1.LogService", "Tail", request)
.await?;
// Client automatically tracks received messages
// and grants credits to server
while let Some(response) = stream.next().await {
let response = response?;
// Process response
// Credits are granted automatically every DEFAULT_CREDIT_REFILL messages
}
See Also¶
- RFC 9113 - HTTP/2 Flow Control
- gRPC Flow Control
crates/quill-core/src/framing.rs- Frame protocol implementationcrates/quill-core/src/flow_control.rs- Credit tracking implementation