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

Make it possible to guarantee that final DATA frame is marked end-of-stream #2

Merged
merged 7 commits into from
Jun 12, 2024

Conversation

edsko
Copy link
Collaborator

@edsko edsko commented Jun 7, 2024

Prior to this PR, when the thread constructing the streaming body of a request (that is, the argument to requestStreaming or requestStreamingUnmask) terminated without calling flush, the final DATA frame would usually be marked as end-of-stream, but there was no guarantee that this would happen. Specifically, this depended on whether the StreamingFinished chunk would be enqueued before some other thread interrupted (perhaps because of a window update):

https://github.com/kazu-yamamoto/http2/blob/398a5c5284421399a6b91a12734a2293a78c0a1b/Network/HTTP2/Client/Run.hs#L206-L211

If this chunk happened to be enqueued a bit later, the result would be that the constructed DATA frame would not be marked as end-of-stream, but instead the stream would terminate with a separate empty data frame. Unfortunately, this confuses some servers, so we need a way to have more control over this.

This PR (along with a companion small PR for http2) achieves this goal in a few steps:

  • First, we introduce requestStreamingIface. I was worried that we'd end up with a whole family of functions requestStreaming, requestStreamingUnmask, requestStreamingThisThatOrTheOther; so to avoid this, requestStreamingIface instead provides the callback with a record of functions, which we can add new features to at will without breaking backwards compatibility and without having to invent new names.
  • Then some refactoring: we document StreamingChunk, split fillBufBuilder (see comments inline for the commits), simplify DynaNext (again, comments in the commit), and refactor fillStreamBodyGetNext (see commit). These changes just pave the way for the "real" change in the next commit.
  • We add an argument to StreamingBuilder to indicate that this Builder is the end-of-stream. This is the direct send equivalent of what we did for receive in Mark final chunk as final #1.
  • Finally, we add outBodyPushFinal to OutBodyIface, which can then be populated in http2, making use of the new argument to StreamingBuilder.

edsko added 7 commits June 7, 2024 17:50
The idea of this version is that we can extend it further without having to
keep introducing new functions all the time: we can just extend the record,
without breaking backwards compatibility.
This avoids this `error` case:

```haskell
    LZero -> error "fillBufBuilder: LZero"
```

but more importantly, paves the way for the next commit.
We were doing the same thing in all cases: compute the `room` as the minimum of
the buffer size and the available window size. Now we just specify this `room`
directly.
This refactored version improves separation of concerns:

* (Only) `fillStreamBodyGetNext.loop` deals with reading the next
  `StreamingChunk` from the queue
* (Only) `runStreamingChunk` deals with the various kinds of `StreamingChunk`s
* (Only) `runStreamingBuilder` deals with the various ways that a `Builder` can
  can terminate.

This obsoletes the need for the `Leftover` data type. This refactoring is
semantics preserving, but paves the way for the next commit.
See justification in the code comments.
After the previous refactoring, this change is now very clean.
This finally is really the purpose of all of these changes; of course, we'll
have to modify `http2` to actually make use of this new function.
@kazu-yamamoto
Copy link
Owner

This PR breaks http3.
I should fix it before releasing a new http-semantics.

@edsko
Copy link
Collaborator Author

edsko commented Jun 8, 2024

Do you mean "break" as in: it needs to be updated because it doesn't compile anymore, or do you mean you updated it and the tests break? The latter would be worrying. If the former, there are three changes that you need to make:

  • DynaNext just wants the available room now, rather than the buffer limit and window size separately; in http2 this is a single change:
-            Next datPayloadLen reqflush mnext <- curr datBuf datBufSiz lim
+            Next datPayloadLen reqflush mnext <- curr datBuf (min datBufSiz lim)
  • You need to provide the Nothing argument to StreamingBuilder in the server (we could think about introducing the same kind of generalization server-side that we did client-side, but I haven't done that, and so server-side we cannot mark the final data frame as final):
-                atomically $ writeTBQueue tbq (StreamingBuilder b)
+                atomically $ writeTBQueue tbq (StreamingBuilder b Nothing)
  • OutBodyStreamingUnmask now wants an OutbodyIface rather than the three separate callbacks:
-        let push b = atomically $ writeTBQueue tbq (StreamingBuilder b)
-            flush = atomically $ writeTBQueue tbq StreamingFlush
-            finished = atomically $ writeTBQueue tbq $ StreamingFinished (decCounter mgr)
+        decrementedCounter <- newIORef False
+        let decCounterOnce = do
+              alreadyDecremented <- atomicModifyIORef decrementedCounter $ \b -> (True, b)
+              unless alreadyDecremented $ decCounter mgr
+        let iface = OutBodyIface {
+                outBodyUnmask = unmask
+              , outBodyPush = \b -> atomically $ writeTBQueue tbq (StreamingBuilder b Nothing)
+              , outBodyPushFinal = \b -> atomically $ writeTBQueue tbq (StreamingBuilder b (Just decCounterOnce))
+              , outBodyFlush = atomically $ writeTBQueue tbq StreamingFlush
+              }
+            finished = atomically $ writeTBQueue tbq $ StreamingFinished decCounterOnce
         incCounter mgr
-        strmbdy unmask push flush `finally` finished
+        strmbdy iface `finally` finished

I think that should be it. Let me know if you'd like me to take a look instead.

@kazu-yamamoto
Copy link
Owner

The former. Thank you for the diffs. I will try to fix http3 on Monday.

@edsko
Copy link
Collaborator Author

edsko commented Jun 8, 2024

Ok, perfect. Thanks @kazu-yamamoto ! Have a great weekend :)

Copy link
Owner

@kazu-yamamoto kazu-yamamoto left a comment

Choose a reason for hiding this comment

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

http3 is also ready.

@kazu-yamamoto kazu-yamamoto merged commit 1a449a4 into kazu-yamamoto:main Jun 12, 2024
@kazu-yamamoto
Copy link
Owner

Merged.
I will release a new version soon.

@edsko edsko deleted the edsko/outbodyiface branch June 12, 2024 10:42
@edsko
Copy link
Collaborator Author

edsko commented Jun 14, 2024

Thanks @kazu-yamamoto ! Pleasure working with you as always :)

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.

2 participants