Skip to content

Conversation

@graebm
Copy link
Contributor

@graebm graebm commented Mar 17, 2020

Summary:

The existing encoder was not streaming. It would error if it didn't have enough space, but there was no way to know how much space it would need. We couldn't just try to re-encode the frame again when more space was available, because header-encoding mutates the HPACK state.

Now we have a "partially" streaming encoder. Most frames are pre-encoded, and we stream them into the available buffers. Headers and Data frames, which may need to split their payload across multiple frames, know how to do so.

Details

  • aws_h2_frame is a proper base class:
    • Previously, each specific frame type had its own init()/encode()/clean_up(). Now it's heap allocated and has vtable for encode()/destroy()
  • Simple frame types (most) make use of the aws_h2_frame_prebuilt type.
    • The entire frame is pre-encoded.
    • We stream the pre-encoded frame into the available space of an aws_io_message.
  • Remove "continuation" aws_h2_frame type.
    • Instead, the "headers" and "push promise" aws_h2_frame types will split their payload across CONTINUATION frames if necessary during encoding.
  • Remove "data" aws_h2_frame type.
    • Instead, the encoder has a special function that takes a body-stream and writes a DATA frame immediately.
    • There are lots of differences in how the connection deals with DATA vs every other frame type, so it didn't make sense for it to be a proper aws_h2_frame type.
  • HPACK encoding will grow the output buffer if necessary.
    • This was the simpler than building a fully "streaming" encoder.
    • HEADERS frame encoding is therefore 2 stage.
      • First, do HPACK encoding to separate buffer.
      • Then, split that payload across multiple HEADERS/CONTINUATION frames as necessary.
  • Moved logic for encoding the header-block into hpack.c. Previously a bunch of that logic was in h2_frames.c. All this code was changing anyway.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

graebm and others added 16 commits March 9, 2020 12:01
Do encoding in 2 passes, so we know exactly how much buffer space we'll need. I will probably revert this. I thought this was clever, but seeing it implemented it's just so much more complicated than writing everything to a dynamic buffer, then copying it into the aws_io_mesage.
ditch 2 pass approach, it was just too complicated
* Enabled compilation on VS 2015

* Fix VS narrowing warning

* Updated to v0.5.3 of builder
Ignore connection: close on 200/OK responses to a CONNECT Request, since the proxy is obviously drunk and needs to hail an uber to get home from the bar safely.

Fix the broken tests from the tcp back pressure refactor in aws-c-io.
@graebm graebm marked this pull request as ready for review March 19, 2020 03:10
graebm added 4 commits March 19, 2020 13:56
- Use common struct
- Pre-encode the entire frame
- Incrementally copy that to aws_io_message whenever encode() is called.

This is simpler/better because:
1) more shared code
2) unique payload-writing code all goes in the one new() function, instead of being spread across the new() and encode() functions
3) less chance of incorrect size calculations, since we're encoding to a buffer of the exact correct length
…If this kind of error happens now, it's programmer error
/* If debug_data is too long, don't sent it.
* It's more important that the GOAWAY frame gets sent. */
const size_t debug_data_max = s_prebuilt_payload_max() - s_frame_goaway_length_min;
if (debug_data.len > debug_data_max) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would truncation be reasonable here? What would this payload look like?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think truncation is probably bad. Imagine owning the xml or json parser on the other side?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users are free to send whatever they want as the debug data.

Henso and I had a big chat about how to handle debug-data being too long.
We decided it shouldn't be a show-stopping error, it's more important that the GOAWAY frame gets sent.

We were less certain about truncating vs sending nothing when it's too long. Someone might never see their data get too long until the magic day that it does. So do we send partial data, or none at all? Partial XML or JSON would turn into a parse error, but a truncated log ... it might not be clear that it's been truncated and just be misleading. Options I can think of:

  1. send no debug-data
  2. truncate debug-data
  3. truncate and write "TRUNCATED" as the last 9 bytes?

THoughts? We can always change this later. It's not exposed all the way out to a public API yet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jonathan chimed in below and voted again for no data

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, thanks for clarifying.


/* If for whatever reason this new header is bigger than the total table size, burn everything to the ground. */
if (AWS_UNLIKELY(header_size > context->dynamic_table.max_size)) {
/* #TODO handle this. It's not an error. It should simply result in an empty table RFC-7541 4.4 */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it hard to clear the table? What makes this a TODO?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't touching the table code in this PR
I just traced my way in here and got spooked, so leaving a todo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👻

@justinboswell
Copy link
Contributor

Pulling out all those checks is NIIIIICE

Copy link
Contributor

@JonathanHenson JonathanHenson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn good work!

struct aws_h2_frame *aws_h2_frame_new_ping(
struct aws_allocator *allocator,
bool ack,
const uint8_t opaque_data[AWS_H2_PING_DATA_SIZE]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you sure this wouldn't be better as a byte_cursor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to be exactly 8 bytes.
I thought this was a cool way to enforce that.
This isn't a public API anyway

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

* This only controls how string values are encoded when they're not already in a table.
*/
enum aws_hpack_huffman_mode {
AWS_HPACK_HUFFMAN_SMALLEST,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, I don't know why we'd want ALWAYS at this point. I like this feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the ALWAYS option is just in there so we can test against some of the samples in RFC-7541

[AWS_H2_SETTINGS_HEADER_TABLE_SIZE] = 4096,
[AWS_H2_SETTINGS_ENABLE_PUSH] = 1,
[AWS_H2_SETTINGS_MAX_CONCURRENT_STREAMS] = UINT32_MAX, /* "Initially there is no limit to this value" */
[AWS_H2_SETTINGS_INITIAL_WINDOW_SIZE] = 65535,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: maybe make these constants that signify their meaning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was doing? you'd access them like so:
aws_h2_settings_initial[AWS_H2_SETTINGS_HEADER_TABLE_SIZE]

instead of:
AWS_H2_HEADER_TABLE_SIZE_INITIAL

I thought it would be more "programmable" to have them in an array.
I'll change it in the future if it's just a pain to read

uint8_t pad_length; /* Set to 0 to disable AWS_H2_FRAME_F_PADDED */

/* HEADERS-only data */
bool end_stream; /* AWS_H2_FRAME_F_END_STREAM */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trivial, maybe take struct packing into account here and group the 1 byte sized members into groups of 2 or 4, or move them to the end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I shuffled them a bit,
I didn't want to lose the grouping by category

flags |= AWS_H2_FRAME_F_END_HEADERS;
} else {
/* If we're not finishing the header-block, is it even worth trying to send this frame now? */
const size_t even_worth_sending_threshold = s_frame_prefix_length + payload_overhead;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could probably make this a bit smarter in the future. Currently this would just be, do we at least have enough space to send a valid but empty headers frame?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I can easily do that.
I was trying to avoid sending a 9-byte HEADERS frame that had no actual header-data in it, knowing that the next time we try to encode there will be a fresh new aws_io_message with tons of space.

Do you think it's valuable to send the valid-but-empty frame earlier?


return AWS_OP_SUCCESS;
/* Write as much of the pre-encoded frame as will fit */
size_t chunk_len = aws_min_size(frame->send_progress.len, output->capacity - output->len);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why isn't this ALWAYS zero? I think Justin and I had the same question... maybe some comments on this somewhere for future posterity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed some variable names and added comments.
Hope this is less confusing now

/* If debug_data is too long, don't sent it.
* It's more important that the GOAWAY frame gets sent. */
const size_t debug_data_max = s_prebuilt_payload_max() - s_frame_goaway_length_min;
if (debug_data.len > debug_data_max) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think truncation is probably bad. Imagine owning the xml or json parser on the other side?

Copy link
Contributor

@justinboswell justinboswell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy with the updates, just one last issue

Copy link
Contributor

@justinboswell justinboswell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PERFECT 👨‍🍳 💋

@graebm graebm merged commit 64aa5fb into master Mar 20, 2020
@graebm graebm deleted the encoder-revamp branch March 20, 2020 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants