diff --git a/src/client.rs b/src/client.rs index f5ce1406..cd42d734 100644 --- a/src/client.rs +++ b/src/client.rs @@ -343,6 +343,11 @@ pub struct Builder { /// /// When this gets exceeded, we issue GOAWAYs. local_max_error_reset_streams: Option, + + /// connection-level budget for DATA framing overhead. + /// + /// When this gets exhausted, we issue a GOAWAY with `ENHANCE_YOUR_CALM`. + data_frame_budget: usize, } #[derive(Debug)] @@ -663,6 +668,7 @@ impl Builder { settings: Default::default(), stream_id: 1.into(), local_max_error_reset_streams: Some(proto::DEFAULT_LOCAL_RESET_COUNT_MAX), + data_frame_budget: proto::DEFAULT_DATA_FRAME_BUDGET, } } @@ -1145,6 +1151,26 @@ impl Builder { self } + /// HTTP/2 flow control limits DATA payload bytes, but it does not limit the number of frames carrying those bytes. + /// A peer could fragment data into many tiny frames, causing disproportionate memory usage from queued events and + /// slab entries while remaining within the flow-control windows. + /// + /// This set a connection-level budget for DATA framing overhead. Small frames consume budget according + /// to the difference between their payload length and the approximate cost of a buffered event. + /// Larger frames replenish the budget, up to its original limit. When the application consumes a queued small frame, + /// its buffering charge is also returned. + /// + /// Non-final DATA frames with an empty decoded payload are discarded after their flow-control accounting is handled. + /// Because they are never exposed to the application, their budget is not returned. + /// + /// Exhausting the budget closes the connection with ENHANCE_YOUR_CALM. + /// + /// Default 25600 bytes + pub fn data_frame_budget(&mut self, budget: usize) -> &mut Self { + self.data_frame_budget = budget; + self + } + /// Sets the first stream ID to something other than 1. #[cfg(feature = "unstable")] pub fn initial_stream_id(&mut self, stream_id: u32) -> &mut Self { @@ -1335,6 +1361,7 @@ where remote_reset_stream_max: builder.pending_accept_reset_stream_max, local_error_reset_streams_max: builder.local_max_error_reset_streams, settings: builder.settings, + data_frame_budget: builder.data_frame_budget, }, ); let send_request = SendRequest { diff --git a/src/proto/connection.rs b/src/proto/connection.rs index 75b77011..26537a8d 100644 --- a/src/proto/connection.rs +++ b/src/proto/connection.rs @@ -83,6 +83,7 @@ pub(crate) struct Config { pub remote_reset_stream_max: usize, pub local_error_reset_streams_max: Option, pub settings: frame::Settings, + pub data_frame_budget: usize, } #[derive(Debug)] @@ -123,6 +124,7 @@ where .max_concurrent_streams() .map(|max| max as usize), local_max_error_reset_streams: config.local_error_reset_streams_max, + data_frame_budget: config.data_frame_budget, } } let streams = Streams::new(streams_config(&config)); diff --git a/src/proto/streams/counts.rs b/src/proto/streams/counts.rs index db1b8220..1980e73b 100644 --- a/src/proto/streams/counts.rs +++ b/src/proto/streams/counts.rs @@ -67,7 +67,7 @@ pub(super) struct Counts { /// lifetime of the connection. num_local_error_reset_streams: usize, - /// Credit for receiving DATA frames without excessive framing overhead. + /// connection-level budget for DATA framing overhead. data_frame_budget: Budget, } @@ -86,7 +86,7 @@ impl Counts { num_remote_reset_streams: 0, max_local_error_reset_streams: config.local_max_error_reset_streams, num_local_error_reset_streams: 0, - data_frame_budget: Budget::new(DEFAULT_DATA_FRAME_BUDGET), + data_frame_budget: Budget::new(config.data_frame_budget), } } @@ -356,6 +356,7 @@ mod tests { remote_init_window_sz: DEFAULT_INITIAL_WINDOW_SIZE, remote_max_initiated: None, local_max_error_reset_streams: None, + data_frame_budget: DEFAULT_DATA_FRAME_BUDGET, }, ) } diff --git a/src/proto/streams/mod.rs b/src/proto/streams/mod.rs index 411a759f..c0845507 100644 --- a/src/proto/streams/mod.rs +++ b/src/proto/streams/mod.rs @@ -78,6 +78,11 @@ pub struct Config { /// /// When this gets exceeded, we issue GOAWAYs. pub local_max_error_reset_streams: Option, + + /// connection-level budget (in bytes) for DATA framing overhead. + /// + /// Default 25600 bytes + pub data_frame_budget: usize, } trait DebugStructExt<'a, 'b> { diff --git a/src/proto/streams/recv.rs b/src/proto/streams/recv.rs index 6d0bd81a..c56b368f 100644 --- a/src/proto/streams/recv.rs +++ b/src/proto/streams/recv.rs @@ -1318,6 +1318,7 @@ mod tests { remote_init_window_sz: DEFAULT_INITIAL_WINDOW_SIZE, remote_max_initiated: None, local_max_error_reset_streams: None, + data_frame_budget: DEFAULT_DATA_FRAME_BUDGET, }; let mut recv = Recv::new(peer::Dyn::Server, &config); let mut store = Store::new(); diff --git a/src/server.rs b/src/server.rs index da6f259b..381a6e9c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -258,6 +258,11 @@ pub struct Builder { /// /// When this gets exceeded, we issue GOAWAYs. local_max_error_reset_streams: Option, + + /// connection-level budget for DATA framing overhead. + /// + /// When this gets exhausted, we issue a GOAWAY with `ENHANCE_YOUR_CALM`. + data_frame_budget: usize, } /// Send a response back to the client @@ -655,8 +660,8 @@ impl Builder { settings: Settings::default(), initial_target_connection_window_size: None, max_send_buffer_size: proto::DEFAULT_MAX_SEND_BUFFER_SIZE, - local_max_error_reset_streams: Some(proto::DEFAULT_LOCAL_RESET_COUNT_MAX), + data_frame_budget: proto::DEFAULT_DATA_FRAME_BUDGET, } } @@ -1040,6 +1045,26 @@ impl Builder { self } + /// HTTP/2 flow control limits DATA payload bytes, but it does not limit the number of frames carrying those bytes. + /// A peer could fragment data into many tiny frames, causing disproportionate memory usage from queued events and + /// slab entries while remaining within the flow-control windows. + /// + /// This set a connection-level budget for DATA framing overhead. Small frames consume budget according + /// to the difference between their payload length and the approximate cost of a buffered event. + /// Larger frames replenish the budget, up to its original limit. When the application consumes a queued small frame, + /// its buffering charge is also returned. + /// + /// Non-final DATA frames with an empty decoded payload are discarded after their flow-control accounting is handled. + /// Because they are never exposed to the application, their budget is not returned. + /// + /// Exhausting the budget closes the connection with ENHANCE_YOUR_CALM. + /// + /// Default 25600 bytes + pub fn data_frame_budget(&mut self, budget: usize) -> &mut Self { + self.data_frame_budget = budget; + self + } + /// Creates a new configured HTTP/2 server backed by `io`. /// /// It is expected that `io` already be in an appropriate state to commence @@ -1505,6 +1530,7 @@ where .builder .local_max_error_reset_streams, settings: self.builder.settings.clone(), + data_frame_budget: self.builder.data_frame_budget, }, );