Skip to content

Commit 1a3614f

Browse files
justinboswellJustin BoswellTingDaoK
authored
Added aws_http2_stream_write_data, allowing H2 data frames to be written at any time (#338)
Adds support for a new H2 API: aws_http2_stream_write_data which allows applications to supply data as it comes in, rather than supplying a stream that is constantly polled and is usually empty. This should allow use cases like event streams and transcribe to be efficient and scalable. Co-authored-by: Justin Boswell <[email protected]> Co-authored-by: Dengke Tang <[email protected]>
1 parent 9717eed commit 1a3614f

9 files changed

+658
-69
lines changed

include/aws/http/private/h2_connection.h

+11-2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ struct aws_h2_connection {
7070
* Waiting for WINDOW_UPDATE to set them free */
7171
struct aws_linked_list stalled_window_streams_list;
7272

73+
/* List using aws_h2_stream.node.
74+
* Contains all streams that are open, but are only sending data when notified, rather than polling
75+
* for it (e.g. event streams)
76+
* Streams are moved to the outgoing_streams_list until they send pending data, then are moved back
77+
* to this list to sleep until more data comes in
78+
*/
79+
struct aws_linked_list waiting_streams_list;
80+
7381
/* List using aws_h2_frame.node.
7482
* Queues all frames (except DATA frames) for connection to send.
7583
* When queue is empty, then we send DATA frames from the outgoing_streams_list */
@@ -200,8 +208,9 @@ enum aws_h2_stream_closed_when {
200208
enum aws_h2_data_encode_status {
201209
AWS_H2_DATA_ENCODE_COMPLETE,
202210
AWS_H2_DATA_ENCODE_ONGOING,
203-
AWS_H2_DATA_ENCODE_ONGOING_BODY_STALLED,
204-
AWS_H2_DATA_ENCODE_ONGOING_WINDOW_STALLED,
211+
AWS_H2_DATA_ENCODE_ONGOING_BODY_STREAM_STALLED, /* stalled reading from body stream */
212+
AWS_H2_DATA_ENCODE_ONGOING_WAITING_FOR_WRITES, /* waiting for next manual write */
213+
AWS_H2_DATA_ENCODE_ONGOING_WINDOW_STALLED, /* stalled due to reduced window size */
205214
};
206215

207216
/* When window size is too small to fit the possible padding into it, we stop sending data and wait for WINDOW_UPDATE */

include/aws/http/private/h2_stream.h

+37-7
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ enum aws_h2_stream_api_state {
5353
AWS_H2_STREAM_API_STATE_COMPLETE,
5454
};
5555

56+
/* Indicates the state of the body of the HTTP/2 stream */
57+
enum aws_h2_stream_body_state {
58+
AWS_H2_STREAM_BODY_STATE_NONE, /* Has no body for the HTTP/2 stream */
59+
AWS_H2_STREAM_BODY_STATE_WAITING_WRITES, /* Has no active body, but waiting for more to be
60+
write */
61+
AWS_H2_STREAM_BODY_STATE_ONGOING, /* Has active ongoing body */
62+
};
63+
64+
/* represents a write operation, which will be turned into a data frame */
65+
struct aws_h2_stream_data_write {
66+
struct aws_linked_list_node node;
67+
struct aws_input_stream *data_stream;
68+
aws_http2_stream_write_data_complete_fn *on_complete;
69+
void *user_data;
70+
bool end_stream;
71+
};
72+
5673
struct aws_h2_stream {
5774
struct aws_http_stream base;
5875

@@ -68,7 +85,16 @@ struct aws_h2_stream {
6885
* We leave it up to the remote peer to detect whether the max window size has been exceeded. */
6986
int64_t window_size_self;
7087
struct aws_http_message *outgoing_message;
88+
/* All queued writes. If the message provides a body stream, it will be first in this list
89+
* This list can drain, which results in the stream being put to sleep (moved to waiting_streams_list in
90+
* h2_connection). */
91+
struct aws_linked_list outgoing_writes; /* aws_http2_stream_data_write */
7192
bool received_main_headers;
93+
94+
/* Indicates that the stream is currently in the waiting_streams_list and is
95+
* asleep. When stream needs to be awaken, moving the stream back to the outgoing_streams_list and set this bool
96+
* to false */
97+
bool waiting_for_writes;
7298
} thread_data;
7399

74100
/* Any thread may touch this data, but the lock must be held (unless it's an atomic) */
@@ -84,10 +110,15 @@ struct aws_h2_stream {
84110
* code we want to inform user about. */
85111
struct aws_h2err reset_error;
86112
bool reset_called;
113+
bool manual_write_ended;
87114

88115
/* Simplified stream state. */
89116
enum aws_h2_stream_api_state api_state;
117+
118+
/* any data streams sent manually via aws_http2_stream_write_data */
119+
struct aws_linked_list pending_write_list; /* aws_h2_stream_pending_data */
90120
} synced_data;
121+
bool manual_write;
91122

92123
/* Store the sent reset HTTP/2 error code, set to -1, if none has sent so far */
93124
int64_t sent_reset_error_code;
@@ -113,16 +144,15 @@ enum aws_h2_stream_state aws_h2_stream_get_state(const struct aws_h2_stream *str
113144
struct aws_h2err aws_h2_stream_window_size_change(struct aws_h2_stream *stream, int32_t size_changed, bool self);
114145

115146
/* Connection is ready to send frames from stream now */
116-
int aws_h2_stream_on_activated(struct aws_h2_stream *stream, bool *out_has_outgoing_data);
147+
int aws_h2_stream_on_activated(struct aws_h2_stream *stream, enum aws_h2_stream_body_state *body_state);
148+
149+
/* Completes stream for one reason or another, clean up any pending writes/resources. */
150+
void aws_h2_stream_complete(struct aws_h2_stream *stream, int error_code);
117151

118152
/* Connection is ready to send data from stream now.
119153
* Stream may complete itself during this call.
120-
* data_encode_status:
121-
* AWS_H2_DATA_ENCODE_COMPLETE: Finished encoding data for the stream
122-
* AWS_H2_DATA_ENCODE_ONGOING: Stream has more data to send.
123-
* AWS_H2_DATA_ENCODE_ONGOING_BODY_STALLED: Stream has more data to send, but it's not ready right now
124-
* AWS_H2_DATA_ENCODE_ONGOING_WINDOW_STALLED: Stream has more data to send but its window size is too small, and stream
125-
* will be moved to stalled_window_stream_list */
154+
* data_encode_status: see `aws_h2_data_encode_status`
155+
*/
126156
int aws_h2_stream_encode_data_frame(
127157
struct aws_h2_stream *stream,
128158
struct aws_h2_frame_encoder *encoder,

include/aws/http/private/request_response_impl.h

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ struct aws_http_stream_vtable {
2323
int (*http2_reset_stream)(struct aws_http_stream *http2_stream, uint32_t http2_error);
2424
int (*http2_get_received_error_code)(struct aws_http_stream *http2_stream, uint32_t *http2_error);
2525
int (*http2_get_sent_error_code)(struct aws_http_stream *http2_stream, uint32_t *http2_error);
26+
int (*http2_write_data)(
27+
struct aws_http_stream *http2_stream,
28+
const struct aws_http2_stream_write_data_options *options);
2629
};
2730

2831
/**

include/aws/http/request_response.h

+95-3
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ struct aws_http_make_request_options {
233233
* See `aws_http_on_stream_complete_fn`.
234234
*/
235235
aws_http_on_stream_complete_fn *on_complete;
236+
237+
/**
238+
* When using HTTP/2, request body data will be provided over time. The stream will only be polled for writing
239+
* when data has been supplied via `aws_http2_stream_write_data`
240+
*/
241+
bool http2_use_manual_data_writes;
236242
};
237243

238244
struct aws_http_request_handler_options {
@@ -286,6 +292,21 @@ struct aws_http_request_handler_options {
286292
aws_http_on_stream_complete_fn *on_complete;
287293
};
288294

295+
/**
296+
* Invoked when the data stream of an outgoing HTTP write operation is no longer in use.
297+
* This is always invoked on the HTTP connection's event-loop thread.
298+
*
299+
* @param stream HTTP-stream this write operation was submitted to.
300+
* @param error_code If error_code is AWS_ERROR_SUCCESS (0), the data was successfully sent.
301+
* Any other error_code indicates that the HTTP-stream is in the process of terminating.
302+
* If the error_code is AWS_ERROR_HTTP_STREAM_HAS_COMPLETED,
303+
* the stream's termination has nothing to do with this write operation.
304+
* Any other non-zero error code indicates a problem with this particular write
305+
* operation's data.
306+
* @param user_data User data for this write operation.
307+
*/
308+
typedef void aws_http_stream_write_complete_fn(struct aws_http_stream *stream, int error_code, void *user_data);
309+
289310
/**
290311
* Invoked when the data of an outgoing HTTP/1.1 chunk is no longer in use.
291312
* This is always invoked on the HTTP connection's event-loop thread.
@@ -298,7 +319,7 @@ struct aws_http_request_handler_options {
298319
* Any other non-zero error code indicates a problem with this particular chunk's data.
299320
* @param user_data User data for this chunk.
300321
*/
301-
typedef void aws_http1_stream_write_chunk_complete_fn(struct aws_http_stream *stream, int error_code, void *user_data);
322+
typedef aws_http_stream_write_complete_fn aws_http1_stream_write_chunk_complete_fn;
302323

303324
/**
304325
* HTTP/1.1 chunk extension for chunked encoding.
@@ -356,6 +377,50 @@ struct aws_http1_chunk_options {
356377
void *user_data;
357378
};
358379

380+
/**
381+
* Invoked when the data of an outgoing HTTP2 data frame is no longer in use.
382+
* This is always invoked on the HTTP connection's event-loop thread.
383+
*
384+
* @param stream HTTP2-stream this write was submitted to.
385+
* @param error_code If error_code is AWS_ERROR_SUCCESS (0), the data was successfully sent.
386+
* Any other error_code indicates that the HTTP-stream is in the process of terminating.
387+
* If the error_code is AWS_ERROR_HTTP_STREAM_HAS_COMPLETED,
388+
* the stream's termination has nothing to do with this write.
389+
* Any other non-zero error code indicates a problem with this particular write's data.
390+
* @param user_data User data for this write.
391+
*/
392+
typedef aws_http_stream_write_complete_fn aws_http2_stream_write_data_complete_fn;
393+
394+
/**
395+
* Encoding options for manual H2 data frame writes
396+
*/
397+
struct aws_http2_stream_write_data_options {
398+
/**
399+
* The data to be sent.
400+
* Optional.
401+
* If not set, input stream with length 0 will be used.
402+
*/
403+
struct aws_input_stream *data;
404+
405+
/**
406+
* Set true when it's the last chunk to be sent.
407+
* After a write with end_stream, no more data write will be accepted.
408+
*/
409+
bool end_stream;
410+
411+
/**
412+
* Invoked when the data stream is no longer in use, whether or not it was successfully sent.
413+
* Optional.
414+
* See `aws_http2_stream_write_data_complete_fn`.
415+
*/
416+
aws_http2_stream_write_data_complete_fn *on_complete;
417+
418+
/**
419+
* User provided data passed to the on_complete callback on its invocation.
420+
*/
421+
void *user_data;
422+
};
423+
359424
#define AWS_HTTP_REQUEST_HANDLER_OPTIONS_INIT \
360425
{ .self_size = sizeof(struct aws_http_request_handler_options), }
361426

@@ -728,9 +793,37 @@ AWS_HTTP_API int aws_http1_stream_write_chunk(
728793
struct aws_http_stream *http1_stream,
729794
const struct aws_http1_chunk_options *options);
730795

796+
/**
797+
* The stream must have specified `http2_use_manual_data_writes` during request creation.
798+
* For client streams, activate() must be called before any frames are submitted.
799+
* For server streams, the response headers must be submitted before any frames.
800+
* A write with options that has end_stream set to be true will end the stream and prevent any further write.
801+
*
802+
* @return AWS_OP_SUCCESS if the write was queued
803+
* AWS_OP_ERROR indicating the attempt raised an error code.
804+
* AWS_ERROR_INVALID_STATE will be raised for invalid usage.
805+
* AWS_ERROR_HTTP_STREAM_HAS_COMPLETED will be raised if the stream ended for reasons behind the scenes.
806+
*
807+
* Typical usage will be something like:
808+
* options.http2_use_manual_data_writes = true;
809+
* stream = aws_http_connection_make_request(connection, &options);
810+
* aws_http_stream_activate(stream);
811+
* ...
812+
* struct aws_http2_stream_write_data_options write;
813+
* aws_http2_stream_write_data(stream, &write);
814+
* ...
815+
* struct aws_http2_stream_write_data_options last_write;
816+
* last_write.end_stream = true;
817+
* aws_http2_stream_write_data(stream, &write);
818+
* ...
819+
* aws_http_stream_release(stream);
820+
*/
821+
AWS_HTTP_API int aws_http2_stream_write_data(
822+
struct aws_http_stream *http2_stream,
823+
const struct aws_http2_stream_write_data_options *options);
824+
731825
/**
732826
* Add a list of headers to be added as trailing headers sent after the last chunk is sent.
733-
* The stream must have specified "chunked" in a "transfer-encoding" header. The stream should also have
734827
* a "Trailer" header field which indicates the fields present in the trailer.
735828
*
736829
* Certain headers are forbidden in the trailer (e.g., Transfer-Encoding, Content-Length, Host). See RFC-7541
@@ -750,7 +843,6 @@ AWS_HTTP_API int aws_http1_stream_add_chunked_trailer(
750843
const struct aws_http_headers *trailing_headers);
751844

752845
/**
753-
* Get the message's aws_http_headers.
754846
*
755847
* This datastructure has more functions for inspecting and modifying headers than
756848
* are available on the aws_http_message datastructure.

source/h2_connection.c

+21-11
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ static struct aws_h2_connection *s_connection_new(
331331
aws_linked_list_init(&connection->thread_data.pending_settings_queue);
332332
aws_linked_list_init(&connection->thread_data.pending_ping_queue);
333333
aws_linked_list_init(&connection->thread_data.stalled_window_streams_list);
334+
aws_linked_list_init(&connection->thread_data.waiting_streams_list);
334335
aws_linked_list_init(&connection->thread_data.outgoing_frames_queue);
335336

336337
if (aws_mutex_init(&connection->synced_data.lock)) {
@@ -453,6 +454,7 @@ static void s_handler_destroy(struct aws_channel_handler *handler) {
453454
!aws_hash_table_is_valid(&connection->thread_data.active_streams_map) ||
454455
aws_hash_table_get_entry_count(&connection->thread_data.active_streams_map) == 0);
455456

457+
AWS_ASSERT(aws_linked_list_empty(&connection->thread_data.waiting_streams_list));
456458
AWS_ASSERT(aws_linked_list_empty(&connection->thread_data.stalled_window_streams_list));
457459
AWS_ASSERT(aws_linked_list_empty(&connection->thread_data.outgoing_streams_list));
458460
AWS_ASSERT(aws_linked_list_empty(&connection->synced_data.pending_stream_list));
@@ -794,6 +796,7 @@ static int s_encode_data_from_outgoing_streams(struct aws_h2_connection *connect
794796
AWS_PRECONDITION(aws_channel_thread_is_callers_thread(connection->base.channel_slot->channel));
795797
struct aws_linked_list *outgoing_streams_list = &connection->thread_data.outgoing_streams_list;
796798
struct aws_linked_list *stalled_window_streams_list = &connection->thread_data.stalled_window_streams_list;
799+
struct aws_linked_list *waiting_streams_list = &connection->thread_data.waiting_streams_list;
797800

798801
/* If a stream stalls, put it in this list until the function ends so we don't keep trying to read from it.
799802
* We put it back at the end of function. */
@@ -851,9 +854,13 @@ static int s_encode_data_from_outgoing_streams(struct aws_h2_connection *connect
851854
case AWS_H2_DATA_ENCODE_ONGOING:
852855
aws_linked_list_push_back(outgoing_streams_list, node);
853856
break;
854-
case AWS_H2_DATA_ENCODE_ONGOING_BODY_STALLED:
857+
case AWS_H2_DATA_ENCODE_ONGOING_BODY_STREAM_STALLED:
855858
aws_linked_list_push_back(&stalled_streams_list, node);
856859
break;
860+
case AWS_H2_DATA_ENCODE_ONGOING_WAITING_FOR_WRITES:
861+
stream->thread_data.waiting_for_writes = true;
862+
aws_linked_list_push_back(waiting_streams_list, node);
863+
break;
857864
case AWS_H2_DATA_ENCODE_ONGOING_WINDOW_STALLED:
858865
aws_linked_list_push_back(stalled_window_streams_list, node);
859866
AWS_H2_STREAM_LOG(
@@ -1761,10 +1768,7 @@ static void s_stream_complete(struct aws_h2_connection *connection, struct aws_h
17611768
aws_linked_list_remove(&stream->node);
17621769
}
17631770

1764-
/* Invoke callback */
1765-
if (stream->base.on_complete) {
1766-
stream->base.on_complete(&stream->base, error_code, stream->base.user_data);
1767-
}
1771+
aws_h2_stream_complete(stream, error_code);
17681772

17691773
/* release connection's hold on stream */
17701774
aws_http_stream_release(&stream->base);
@@ -1859,15 +1863,20 @@ static void s_move_stream_to_thread(
18591863
goto error;
18601864
}
18611865

1862-
bool has_outgoing_data = false;
1863-
if (aws_h2_stream_on_activated(stream, &has_outgoing_data)) {
1866+
enum aws_h2_stream_body_state body_state = AWS_H2_STREAM_BODY_STATE_NONE;
1867+
if (aws_h2_stream_on_activated(stream, &body_state)) {
18641868
goto error;
18651869
}
1866-
1867-
if (has_outgoing_data) {
1868-
aws_linked_list_push_back(&connection->thread_data.outgoing_streams_list, &stream->node);
1870+
switch (body_state) {
1871+
case AWS_H2_STREAM_BODY_STATE_WAITING_WRITES:
1872+
aws_linked_list_push_back(&connection->thread_data.waiting_streams_list, &stream->node);
1873+
break;
1874+
case AWS_H2_STREAM_BODY_STATE_ONGOING:
1875+
aws_linked_list_push_back(&connection->thread_data.outgoing_streams_list, &stream->node);
1876+
break;
1877+
default:
1878+
break;
18691879
}
1870-
18711880
return;
18721881
error:
18731882
/* If the stream got into any datastructures, s_stream_complete() will remove it */
@@ -1954,6 +1963,7 @@ static void s_cross_thread_work_task(struct aws_channel_task *task, void *arg, e
19541963
s_send_goaway(connection, goaway->http2_error, goaway->allow_more_streams, &goaway->debug_data);
19551964
aws_mem_release(connection->base.alloc, goaway);
19561965
}
1966+
19571967
/* It's likely that frames were queued while processing cross-thread work.
19581968
* If so, try writing them now */
19591969
aws_h2_try_write_outgoing_frames(connection);

0 commit comments

Comments
 (0)