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

Stream Multiplexing and Flow Control #14

Closed
wants to merge 150 commits into from

Conversation

GallaFrancesco
Copy link
Contributor

This PR proposes a logic for stream multiplexing which should be simple enough to discuss and improve. The changes are focused around the new multiplexing.d module, which contains most of the data structures used to maintain the state of a HTTP/2 connection, while enabling the following features:

Stream multiplexing

Performed using a table of HTTP2Multiplexers which are indexed on a connection-based ID, since the HTTP/2 RFC permits stream multiplexing at connection level. Reference: Section 5, RFC 7540. The process of registering and using a stream multiplexer is the following:

  1. Each time a new HTTP/2 connection is established (http2.d, handleHTTP2Connection) a new multiplexer is registered through the multiplexer() method. This requires an ID for the mux table which must be unique with respect to the connection. This ID is a string generated by connection.localAddress.toString ~ connection.peerAddress, where connection is the underlying TCPConnection struct. Since a TCP connection can be uniquely described by the 4 parameters [local host, local port, remote host, remote port] this is sufficient to guarantee that no equal IDs are generated corresponding to two different connections.
  2. After a frame is received, handleFrameAlloc registers the new stream by calling registerStream with the IDs of the multiplexer and the stream. The HTTP2Multiplexer adds the stream ID to a set of open streams. The stream ID must be unique, to enforce this the set is implemented using a RedBlackTree from phobos.
  3. The frame is processed according to its type (http2.d, handleFrameAlloc) until the stream lifetime is over: the stream is then closed by calling closeStream, which causes the stream ID to be removed from the set of open streams (for an overview of stream lifetimes, see Section 5.1 of RFC 7540). This process (registering a new stream, handling it, closing it) can be repeated indefinitely on the same HTTP2Multiplexer as long as frames are received and the connection is open.
  4. Once a connection has to be closed, the corresponding multiplexer is removed from the table, by calling removeMux with the multiplexer ID.

struct HTTP2Multiplexer embeds the required checks to ensure that the registered streams are valid with respect to the previous one. It does so by keeping track of open stream IDs, the highest open stream ID and checking that clients only provide odd-numbered streams (as specified in RFC 7540). Since we use an asynchronous frame handler (handleHTTP2FrameChain) the operations which require modifications inside the same HTTP2Multiplexer are protected by a TaskMutex.
At the moment, the table of multiplexers is held as a global variable in memory, but I would like to discuss a better/safer approach (even though the handling performed by multiplexer.d is working without memory corruption or data races so far).

Flow control

Since multiplexing streams in HTTP/2 requires a flow control scheme (see Section 5.2 of RFC 7540) struct HTTP2Multiplexer also integrates the information regarding the connection window which the server is allowed to send. The flow control logic works with the following approach:

  1. A client can advertise to the server its maximum control flow window, which is the number of octets that he's prepared to receive as DATA frames, by sending a SETTINGS frame or a WINDOW_UPDATE frame. As the server receives this information, it immediately updates the values mantained in the HTTP2Multiplexer by calling updateConnectionWindow and updateStreamConnectionWindow.
  2. Once the connection window is updated (be it connection-wide or specific to a stream ID) the server checks if there are pending DATA tasks which might be allowed to send one or moder DATA frames depending on the updated connection value. If pending tasks are present, they are resumed using the TaskCondition until the payload dispatch is complete OR the connection window value reaches 0.

To avoid locking the server while a DATA frame has to be sent, the sending logic for DATA frames is built in an asynchronous fashion in exchange.d by sendDataTask(). The task works by sending all the octets allowed by the connection window, exiting successfully if the DATA payload can be sent completely, otherwise splitting the payload in multiple DATA frames and waiting to send until a new WINDOW_UPDATE is received. During this process, the connection window is progressively updated by subtracting the number of octets sent successfully.

Using this logic, the server is able to:

  • Receive multiple requests on different stream IDs, providing an appropriate GOAWAY response if the stream id / frame header is invalid, processing the frames received otherwise (this is done using exceptions, see error.d, in a fashion which is similar to the one used in http1.d, but I agree that there could be more efficient approaches.
  • Keep track of pending DATA tasks, resuming them whenever a client requires it. Note that right now a timeout of 60 seconds removes a pending task if no WINDOW_UPDATE is received, but this behavior can be changed easily.
  • Properly opening / closing streams as requested by the client, including the one caused by resets (RST_STREAM frames) or errors (GOAWAY frames).

What is missing:

  • Stream Priority has yet to be introduced, but at the current stage is not essential for proper HTTP/2 message exchanges. It will be introduced in the next PRs, once the multiplexing logic is agreed upon.
  • PUSH_PROMISE frames
  • A lot of bug fixing and corner cases (I'm working on that by testing with different browsers, h2spec and the h2load benchmarking tool provided by nghttp2. I've also introduces a small web server example in examples/http2 which should demonstrate basic functionality.
  • Optimizing for @nogc behavior is something which requires a lot of changes in the way requests are handled and errors are propagated (HTTPServerRequestData, exceptions) but I'm looking into it. IMHO the best moment to discuss that is when the server is stable enough that we can detect changes which break the functionality more easily.

This PR spans multiple files since 2 small bugs in HPACK have been corrected, but mainly i because tried to introduce as more documentation as possible, in the form of comments and code refactoring, to ease the review (and the developing) process, I hope it's readable enough.

Feel free to prompt me to split this PR if the changes are too many to keep track of, this PR is meant to describe the progress so far and to agree on a multiplexing strategy.
Thanks!

@s-ludwig
Copy link
Member

I finally managed to to read through to the end in a single session, sorry that it took so long! So all in all I think this looks fine. The only thing that struck me as odd was the global dictionary used for the stream IDs. If at all possible, this should be using direct pointers/references (GC or RC) instead of going throgh a string lookup (and string construction).

Other than that, since the only sensible mode of review in this case is to review the whole code at once anyway, I think it makes sense to squash everything together into just two or three commits (one commit for the added functionality and other commits for fixes to existing code).

@GallaFrancesco
Copy link
Contributor Author

I thought about it (sorry if my response took a while). I think the best overall option would be to use reference counting to efficiently manage the multiplexer struct, so that we don't force GC dependence and don't need to dispose of it explicitly as the TCPConnection gets destroyed (we can probably tie it to the HTTP2ServerContext struct).

Only issue with that (and the main reason I choose to adopt a global table as a temporary proof of concept) is that a lot of arguments are passed by copy from one task to the other (see handleHTTP2FrameChain). This has the advantage of reducing heap allocations overall, yet passing a multiplexer struct could exceed the size limit for runTask() and force us to use ref / pointers on HTTP2ServerContext. This could mean having additional consistency checks when the struct gets passed from one task to another, given that it must be persistent through the whole duration of a connection.

I'll work on the refcount approach and rebase, so that we can proceed. Thank you!

@GallaFrancesco
Copy link
Contributor Author

Rebased and updated in #15. Closing this one.

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.

None yet

2 participants