Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Reqd.schedule_trailers (fixes #97) #141

Merged
merged 2 commits into from Sep 28, 2020
Merged

Conversation

blandinw
Copy link
Contributor

@blandinw blandinw commented Sep 27, 2020

This PR makes sending trailing headers (aka. trailers) possible, and thus allows H2 to be used to implement gRPC.
I've been going back and forth on the best patch to write, and I believe this strikes a good balance, but I'm curious to hear your thoughts.
I had a look at the changes proposed in #72 and I believe it suffers from the sequencing issue described below (commit #1).
However, I did steal the test case from there.

Commit 1 (move Streaming flush logic out of Body):

  • Body.close_writer caused stream to close before we had the chance to send headers
  • Body.close_writer caused next flush to send a zero length DATA frame, which leads to a sequence like HEADERS, DATA, TRAILERS, DATA
  • imo, Body should not be concerned with HTTP details (knowing about RFC7540 and sending DATA frames)
  • moving the corresponding code out of Body and into Reqd feels like the right thing to do since it allows for better sequencing + simpler code (has_pending_output forwards to Faraday, no more t.write_final_data_frame and no more response_body_requires_output)
    I also noticed a potential bug where I believe the zero length DATA frame is never sent if we're flow-controlled (maxxed out sent bytes) in Scheduler.write

Commit 2 (allow trailing headers, fixes #97)

  • straight-forward thanks to the sequencing control allowed by commit 1
  • we choose not to let user send trailers directly, but instead register them and later send them on Body.close to avoid user mistakes (sending trailers too early or too late)
  • this also allows to avoid sending a useless zero length DATA frame before the trailers

Quick note: I was confused at the beginning as to how the state machine worked, until I realized it relied on Gluten. It's not obvious at first if you're grepping around (could not find next_write_operation call sites)
You may want to have a quick blurb or schema showing the Gluten -> Server_connection -> Scheduler -> Reqd -> Writer -> Faraday chain.

@anmonteiro
Copy link
Owner

Thanks for sending this. I'll try to get to it this weekend.

Before I start reviewing, a potentially related question: would gRPC also need to read trailer headers, in addition to sending them (what this PR does)? There's some machinery in place for that but it's not fully wired up.

@blandinw
Copy link
Contributor Author

blandinw commented Sep 27, 2020

Hi Antonio, thanks!

Clients need the ability to read the response trailers since they contain the actual status code (HTTP code should always be 200, regardless of errors), and an optional message. Clients do not need the ability to send request trailers (nor do servers need to read request trailers) as far as I know.

@anmonteiro
Copy link
Owner

I also noticed a potential bug where I believe the zero length DATA frame is never sent if we're flow-controlled (maxxed out sent bytes) in Scheduler.write

@blandinw I think you're right about this. If you don't mind, I'm going to push a test + a fix for that before discussing this PR further.

Copy link
Owner

@anmonteiro anmonteiro left a comment

Choose a reason for hiding this comment

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

@blandinw I like the direction of this PR and the approach you took. Thanks again for sending it over!

I only left mostly minor stylistic / documentation comments. Additionally, would you mind adding an entry to the CHANGES.md file?

Note: I force-pushed to this branch after merging #142, so please make sure to reset locally.

lib/h2.mli Outdated Show resolved Hide resolved
lib/reqd.ml Outdated Show resolved Hide resolved
lib/reqd.ml Outdated
let send_trailers_on_close t new_trailers =
let go s =
match s.response_state with
| Streaming (rsp, rsp_body, _old_trailers) ->
Copy link
Owner

Choose a reason for hiding this comment

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

thoughts on failing if you try to schedule multiple times? I think this behavior should be somewhat specified, and additionally documented in the .mli

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 can't think of a use case where we'd need to change the trailers after setting them, and we should notify the user somehow that something is probably wrong.

I was hoping we could somehow avoid runtime errors, but I'm not sure how to do it with the current API. Maybe something to think about if we decide to change the API one day (we could then probably enforce it at the type level).

I'll error out for now.

in
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) | Closed _ ->
assert false
Copy link
Owner

Choose a reason for hiding this comment

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

I think it's possible, in certain cases, to retain a reference to the request descriptor when the stream has been closed (e.g. because of an error). The other cases seem fine to me, but the Closed case should probably be handled separately.

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 fail with a useful error message in the Closed and Reserved cases.
Let me know if this is what you had in mind.

lib/reqd.ml Outdated Show resolved Hide resolved
lib/serialize.ml Show resolved Hide resolved
Alcotest.fail
"Expected state machine to issue a write operation after seeing \
headers."

(* TODO: test for trailer headers. *)
Copy link
Owner

Choose a reason for hiding this comment

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

remove TODO please :)

lib_test/test_h2_server.ml Outdated Show resolved Hide resolved
lib_test/test_h2_server.ml Outdated Show resolved Hide resolved
lib_test/test_h2_server.ml Show resolved Hide resolved
Copy link
Contributor Author

@blandinw blandinw left a comment

Choose a reason for hiding this comment

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

Addressed your comments in a new commit for easier review, please squash before merging

in
match t.state with
| Idle | Active (Open (WaitingForPeer | PartialHeaders _), _) | Closed _ ->
assert false
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 fail with a useful error message in the Closed and Reserved cases.
Let me know if this is what you had in mind.

lib/reqd.ml Outdated
~max_frame_size:t.max_frame_size
~flags:Flags.(set_end_stream default_flags)
t.id
let frame_info flags =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, I used to send an extra DATA frame, but changed my mind.

@blandinw blandinw changed the title add Reqd.send_trailers_on_close (fixes #97) add Reqd.schedule_trailers (fixes #97) Sep 28, 2020
stream.encoder
frame_info
trailers;
close_stream t;
Copy link
Owner

Choose a reason for hiding this comment

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

the previous behavior flushed the writer before marking the stream as closed. Was this change intentional?

Copy link
Contributor Author

@blandinw blandinw Sep 28, 2020

Choose a reason for hiding this comment

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

Here's the recent history for this line of code

dune runtest and my toy gRPC project are running fine, and a quick look at the code makes me think it should work, but I'll defer to you since you're more experienced with the codebase.

Copy link
Owner

Choose a reason for hiding this comment

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

Good point -- I forgot close_stream already flushed.

Copy link
Owner

@anmonteiro anmonteiro left a comment

Choose a reason for hiding this comment

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

@blandinw thank you for addressing my comments. this looks almost ready now! I just left one additional question.

@anmonteiro anmonteiro merged commit e92be08 into anmonteiro:master Sep 28, 2020
@anmonteiro
Copy link
Owner

Thanks again!

@blandinw
Copy link
Contributor Author

Happy to contribute, it was fun 😄

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.

Trailing headers
2 participants