Skip to content

Commit b5587a8

Browse files
authored
HTTP/1 respects the stream window. (#277)
### ISSUE Previously, the "stream window" in HTTP/1 was a lie. There was really just the connection-window. We encountered bugs when a connection was re-used for another stream. The stream **believed** it started with initial-window-size, but really it got whatever connection-window was left over from the previous stream. The connection could stall because the connection-window size was nearly zero, but the stream thought its window was wide open. Likewise, if the previous stream left a larger connection-window, the new stream could receive data that exceeded its initial-window-size. ### SOLUTION Honor the stream window. All new streams start with initial-window-size. Streams cannot receive more body data than their stream-window allows. Users can no longer control the connection window directly, because it is doing gymnastics to honor these contracts. ### DETAILS aws_io_messages are put in a queue before processing. This allows the connection to receive more data than a stream's window might allow, and process the data later when the stream's window opens, or the next stream begins. The connection-window is capped at `read_buffer_capacity`, and shrinks when there is unprocessed data in the queue. users can customize`read_buffer_capacity`, but we'll choose something for them if they leave it 0 in the options.
1 parent b7d1251 commit b5587a8

File tree

14 files changed

+753
-303
lines changed

14 files changed

+753
-303
lines changed

include/aws/http/connection.h

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,27 @@ struct aws_http_proxy_options {
170170
};
171171

172172
/**
173-
* HTTP/2 connection options.
173+
* Options specific to HTTP/1.x connections.
174+
* Initialize with AWS_HTTP1_CONNECTION_OPTIONS_INIT to set default values.
175+
*/
176+
struct aws_http1_connection_options {
177+
/**
178+
* Optional
179+
* Capacity in bytes of the HTTP/1 connection's read buffer.
180+
* The buffer grows if the flow-control window of the incoming HTTP-stream
181+
* reaches zero. If the buffer reaches capacity, no further socket data is
182+
* read until the HTTP-stream's window opens again, allowing data to resume flowing.
183+
*
184+
* Ignored if `manual_window_management` is false.
185+
* If zero is specified (the default) then a default capacity is chosen.
186+
* A capacity that is too small may hinder throughput.
187+
* A capacity that is too big may waste memory without helping throughput.
188+
*/
189+
size_t read_buffer_capacity;
190+
};
191+
192+
/**
193+
* Options specific to HTTP/2 connections.
174194
* Initialize with AWS_HTTP2_CONNECTION_OPTIONS_INIT to set default values.
175195
*/
176196
struct aws_http2_connection_options {
@@ -286,10 +306,24 @@ struct aws_http_client_connection_options {
286306
const struct aws_http_connection_monitoring_options *monitoring_options;
287307

288308
/**
289-
* Optional.
290-
* The initial connection flow-control window size for HTTP/1 connection.
291-
* Ignored by HTTP/2 connection, since the initial connection flow-control window in HTTP/2 is not configurable.
292-
* A default size is set by AWS_HTTP_CLIENT_CONNECTION_OPTIONS_INIT.
309+
* Set to true to manually manage the flow-control window of each stream.
310+
*
311+
* If false, the connection will maintain its flow-control windows such that
312+
* no back-pressure is applied and data arrives as fast as possible.
313+
*
314+
* If true, the flow-control window of each stream will shrink as body data
315+
* is received (headers, padding, and other metadata do not affect the window).
316+
* `initial_window_size` determines the starting size of each stream's window.
317+
* If a stream's flow-control window reaches 0, no further data will be received.
318+
* The user must call aws_http_stream_update_window() to increment the stream's
319+
* window and keep data flowing.
320+
*/
321+
bool manual_window_management;
322+
323+
/**
324+
* The starting size of each HTTP stream's flow-control window.
325+
* Required if `manual_window_management` is true,
326+
* ignored if `manual_window_management` is false.
293327
*/
294328
size_t initial_window_size;
295329

@@ -315,23 +349,20 @@ struct aws_http_client_connection_options {
315349
aws_http_on_client_connection_shutdown_fn *on_shutdown;
316350

317351
/**
318-
* Set to true to manually manage the read window size.
319-
*
320-
* If this is false, the connection will maintain a constant window size.
321-
*
322-
* If this is true, the caller must manually increment the window size using aws_http_stream_update_window().
323-
* If the window is not incremented, it will shrink by the amount of body data received. If the window size
324-
* reaches 0, no further data will be received.
325-
**/
326-
bool manual_window_management;
352+
* Options specific to HTTP/1.x connections.
353+
* Optional.
354+
* Ignored if connection is not HTTP/1.x.
355+
* If connection is HTTP/1.x and options were not specified, default values are used.
356+
*/
357+
const struct aws_http1_connection_options *http1_options;
327358

328359
/**
329-
* HTTP/2 connection specific options.
360+
* Options specific to HTTP/2 connections.
330361
* Optional.
331-
* If HTTP/2 connection created, we will use this for some configurations in HTTP/2 connection.
332-
* If other protocol connection created, this will be ignored.
362+
* Ignored if connection is not HTTP/2.
363+
* If connection is HTTP/2 and options were not specified, default values are used.
333364
*/
334-
struct aws_http2_connection_options *http2_options;
365+
const struct aws_http2_connection_options *http2_options;
335366
};
336367

337368
/* Predefined settings identifiers (RFC-7540 6.5.2) */
@@ -352,18 +383,27 @@ struct aws_http2_setting {
352383
uint32_t value;
353384
};
354385

386+
/**
387+
* Initializes aws_http1_connection_options with default values.
388+
*/
389+
#define AWS_HTTP1_CONNECTION_OPTIONS_INIT \
390+
{ .read_buffer_capacity = 0 }
391+
355392
/**
356393
* HTTP/2: Default value for max closed streams we will keep in memory.
357394
*/
358395
#define AWS_HTTP2_DEFAULT_MAX_CLOSED_STREAMS (32)
396+
359397
/**
360398
* HTTP/2: The size of payload for HTTP/2 PING frame.
361399
*/
362400
#define AWS_HTTP2_PING_DATA_SIZE (8)
401+
363402
/**
364403
* HTTP/2: The number of known settings.
365404
*/
366405
#define AWS_HTTP2_SETTINGS_COUNT (6)
406+
367407
/**
368408
* Initializes aws_http2_connection_options with default values.
369409
*/
@@ -426,7 +466,8 @@ AWS_HTTP_API
426466
bool aws_http_connection_is_client(const struct aws_http_connection *connection);
427467

428468
/**
429-
* Increments the connection-wide read window by the value specified.
469+
* DEPRECATED
470+
* TODO: Delete once this is removed from H2.
430471
*/
431472
AWS_HTTP_API
432473
void aws_http_connection_update_window(struct aws_http_connection *connection, size_t increment_size);

include/aws/http/private/connection_impl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ struct aws_http_client_bootstrap {
132132
aws_http_on_client_connection_shutdown_fn *on_shutdown;
133133
aws_http_proxy_request_transform_fn *proxy_request_transform;
134134

135+
struct aws_http1_connection_options http1_options;
135136
struct aws_http2_connection_options http2_options;
136137
struct aws_http_connection *connection;
137138
};

include/aws/http/private/h1_connection.h

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
struct aws_h1_connection {
1919
struct aws_http_connection base;
2020

21-
size_t initial_window_size;
21+
size_t initial_stream_window_size;
2222

2323
/* Task responsible for sending data.
2424
* As long as there is data available to send, the task will be "active" and repeatedly:
@@ -64,17 +64,33 @@ struct aws_h1_connection {
6464
/* Used to encode requests and responses */
6565
struct aws_h1_encoder encoder;
6666

67-
size_t body_bytes_decoded;
68-
69-
/* All aws_io_messages arriving in the read direction are queued here before processing.
70-
* The downstream window (window of next handler, or window of incoming stream)
71-
* determines how much data can be processed. The `copy_mark` is used to
72-
* track progress on partially processed messages at the front of the queue. */
67+
/**
68+
* All aws_io_messages arriving in the read direction are queued here before processing.
69+
* This allows the connection to receive more data than the the current HTTP-stream might allow,
70+
* and process the data later when HTTP-stream's window opens or the next stream begins.
71+
*
72+
* The `aws_io_message.copy_mark` is used to track progress on partially processed messages.
73+
* `pending_bytes` is the sum of all unprocessed bytes across all queued messages.
74+
* `capacity` is the limit for how many unprocessed bytes we'd like in the queue.
75+
*/
7376
struct {
7477
struct aws_linked_list messages;
75-
/* TODO: more variables will go in this struct in the near-future */
78+
size_t pending_bytes;
79+
size_t capacity;
7680
} read_buffer;
7781

82+
/**
83+
* The connection's current window size.
84+
* We use this variable, instead of the existing `aws_channel_slot.window_size`,
85+
* because that variable is not updated immediately, the channel uses a task to update it.
86+
* Since we use the difference between current and desired window size when deciding
87+
* how much to increment, we need the most up-to-date values possible.
88+
*/
89+
size_t connection_window;
90+
91+
/* Only used by tests. Sum of window_increments issued by this slot. Resets each time it's queried */
92+
size_t recent_window_increments;
93+
7894
struct aws_crt_statistics_http1_channel stats;
7995

8096
uint64_t outgoing_stream_timestamp_ns;
@@ -94,6 +110,8 @@ struct aws_h1_connection {
94110

95111
/* see `outgoing_stream_task` */
96112
bool is_outgoing_stream_task_active : 1;
113+
114+
bool is_processing_read_messages : 1;
97115
} thread_data;
98116

99117
/* Any thread may touch this data, but the lock must be held */
@@ -119,6 +137,16 @@ struct aws_h1_connection {
119137
} synced_data;
120138
};
121139

140+
/* Allow tests to check current window stats */
141+
struct aws_h1_window_stats {
142+
size_t connection_window;
143+
size_t recent_window_increments; /* Resets to 0 each time window stats are queried*/
144+
size_t buffer_capacity;
145+
size_t buffer_pending_bytes;
146+
uint64_t stream_window;
147+
bool has_incoming_stream;
148+
};
149+
122150
AWS_EXTERN_C_BEGIN
123151

124152
/* The functions below are exported so they can be accessed from tests. */
@@ -127,13 +155,19 @@ AWS_HTTP_API
127155
struct aws_http_connection *aws_http_connection_new_http1_1_server(
128156
struct aws_allocator *allocator,
129157
bool manual_window_management,
130-
size_t initial_window_size);
158+
size_t initial_window_size,
159+
const struct aws_http1_connection_options *http1_options);
131160

132161
AWS_HTTP_API
133162
struct aws_http_connection *aws_http_connection_new_http1_1_client(
134163
struct aws_allocator *allocator,
135164
bool manual_window_management,
136-
size_t initial_window_size);
165+
size_t initial_window_size,
166+
const struct aws_http1_connection_options *http1_options);
167+
168+
/* Allow tests to check current window stats */
169+
AWS_HTTP_API
170+
struct aws_h1_window_stats aws_h1_connection_window_stats(struct aws_http_connection *connection_base);
137171

138172
AWS_EXTERN_C_END
139173

@@ -153,4 +187,15 @@ void aws_h1_connection_unlock_synced_data(struct aws_h1_connection *connection);
153187
*/
154188
void aws_h1_connection_try_write_outgoing_stream(struct aws_h1_connection *connection);
155189

190+
/**
191+
* If any read messages are queued, and the downstream window is non-zero,
192+
* process data and send it downstream. Then calculate the connection's
193+
* desired window size and increment it if necessary.
194+
*
195+
* During normal operations "downstream" means the current incoming stream.
196+
* If the connection has switched protocols "downstream" means the next
197+
* channel handler in the read direction.
198+
*/
199+
void aws_h1_connection_try_process_read_messages(struct aws_h1_connection *connection);
200+
156201
#endif /* AWS_HTTP_H1_CONNECTION_H */

include/aws/http/private/h1_stream.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ struct aws_h1_stream {
6262
* Encoder completes/frees/pops front chunk when it's done sending. */
6363
struct aws_linked_list pending_chunk_list;
6464

65+
/* Size of stream's flow-control window.
66+
* Only body data (not headers, etc) counts against the stream's flow-control window. */
67+
uint64_t stream_window;
68+
6569
/* Whether a "request handler" stream has a response to send.
6670
* Has mirror variable in synced_data */
6771
bool has_outgoing_response : 1;
@@ -77,6 +81,9 @@ struct aws_h1_stream {
7781

7882
enum aws_h1_stream_api_state api_state;
7983

84+
/* Sum of all aws_http_stream_update_window() calls that haven't yet moved to thread_data.stream_window */
85+
uint64_t pending_window_update;
86+
8087
/* See `cross_thread_work_task` */
8188
bool is_cross_thread_work_task_scheduled : 1;
8289

include/aws/http/private/request_response_impl.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ struct aws_http_stream {
3535

3636
uint32_t id;
3737

38-
bool manual_window_management;
39-
4038
void *user_data;
4139
aws_http_on_incoming_headers_fn *on_incoming_headers;
4240
aws_http_on_incoming_header_block_done_fn *on_incoming_header_block_done;

include/aws/http/request_response.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -757,11 +757,17 @@ AWS_HTTP_API
757757
int aws_http_stream_send_response(struct aws_http_stream *stream, struct aws_http_message *response);
758758

759759
/**
760-
* Manually issue a window update.
761-
* Note that the stream's default behavior is to issue updates which keep the window at its original size.
762-
* See aws_http_make_request_options.manual_window_management for details on letting the window shrink.
760+
* Increment the stream's flow-control window to keep data flowing.
763761
*
764-
* TODO: HTTP/2 vs HTTP/1, we need two different function
762+
* If the connection was created with `manual_window_management` set true,
763+
* the flow-control window of each stream will shrink as body data is received
764+
* (headers, padding, and other metadata do not affect the window).
765+
* The connection's `initial_window_size` determines the starting size of each stream's window.
766+
* If a stream's flow-control window reaches 0, no further data will be received.
767+
*
768+
* If `manual_window_management` is false, this call will have no effect.
769+
* The connection maintains its flow-control windows such that
770+
* no back-pressure is applied and data arrives as fast as possible.
765771
*/
766772
AWS_HTTP_API
767773
void aws_http_stream_update_window(struct aws_http_stream *stream, size_t increment_size);

0 commit comments

Comments
 (0)