Skip to content

Fixes bi-directional streaming #344

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

Merged
merged 1 commit into from
Mar 3, 2021

Conversation

artemredkin
Copy link
Collaborator

Motivation:
When we stream request body, current implementation expects that body
will finish streaming before we start to receive response body parts.
This is not correct, reponse body parts can start to arrive before we
finish sending the request.

Modifications:

  • Simplifies state machine, we only case about request being fully sent
    to prevent sending body parts after .end, but response state machine
    is mostly ignored and correct flow will be handled by NIOHTTP1
    pipeline
  • Adds HTTPEchoHandler, that replies to each response body part
  • Adds bi-directional streaming test

Result:
Closes #327

@@ -1025,8 +1019,7 @@ extension TaskHandler: ChannelDuplexHandler {
/// Some HTTP Servers can 'forget' to respond with CloseNotify when client is closing connection,
/// this could lead to incomplete SSL shutdown. But since request is already processed, we can ignore this error.
break
case .head where self.ignoreUncleanSSLShutdown,
.body where self.ignoreUncleanSSLShutdown:
case .sending where self.ignoreUncleanSSLShutdown:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

test testNoResponseWithIgnoreErrorForSSLUncleanShutdown fails if I add .sent, I'm not sure why yet...

Copy link
Collaborator Author

@artemredkin artemredkin Mar 3, 2021

Choose a reason for hiding this comment

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

.sending means that we have not sent client .end

Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between .sent and .endOrError? In many cases (content-length present) after having sent the response body, we're done?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you are looking at slightly outdated code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is a slightly weird part, it tries to do the following: if ignoreUncleanSSLShutdown is set and we received response .head, then it's allowed to not fail

@artemredkin artemredkin force-pushed the fix_bi_streaming branch 3 times, most recently from ce06b45 to 88f4dd3 Compare March 3, 2021 11:45
@@ -840,23 +839,20 @@ extension TaskHandler: ChannelDuplexHandler {
self.writeBody(request: request, context: context)
}.flatMap {
context.eventLoop.assertInEventLoop()

if case .endOrError = self.state {
Copy link
Contributor

Choose a reason for hiding this comment

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

could we replace this if case and the fallthrough by one full switch self.state?

It's unclear to me why we can move from almost any state to .sent and why all those transitions are legal.

Copy link
Contributor

Choose a reason for hiding this comment

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

For example a transition from .sent to .sent seems odd, right? Or from .inactive to .sent seems wrong too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

case idle
case bodySent
case inactive
case sending
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 we could come up with slightly more prescriptive names? What are wen sending here? The request, the response, the head, the body, the trailers? Some of them, all of them?

Same for sent

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@@ -902,7 +898,7 @@ extension TaskHandler: ChannelDuplexHandler {

private func writeBodyPart(context: ChannelHandlerContext, part: IOData, promise: EventLoopPromise<Void>) {
switch self.state {
case .idle:
case .sending:
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems a little dangerous? Previously we could send in .idle and now we can't?

Copy link
Contributor

Choose a reason for hiding this comment

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

A full switch without the default would be better too here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@@ -1025,8 +1019,7 @@ extension TaskHandler: ChannelDuplexHandler {
/// Some HTTP Servers can 'forget' to respond with CloseNotify when client is closing connection,
/// this could lead to incomplete SSL shutdown. But since request is already processed, we can ignore this error.
break
case .head where self.ignoreUncleanSSLShutdown,
.body where self.ignoreUncleanSSLShutdown:
case .sending where self.ignoreUncleanSSLShutdown:
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between .sent and .endOrError? In many cases (content-length present) after having sent the response body, we're done?

Copy link
Contributor

@weissi weissi left a comment

Choose a reason for hiding this comment

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

thank you! This looks like a great start, left some comments

case sent
case head
case inactive
case sending_waiting
Copy link
Contributor

Choose a reason for hiding this comment

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

  • we should use lowerCamelCase to match swift's usual style.
  • I think we can do better than sendingWaiting, ie. what are we sending and what are we waiting for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

case bodySent
case sent
case head
case inactive
Copy link
Contributor

Choose a reason for hiding this comment

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

idle is probably better than inactive because the channel is actually isActive = true, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@artemredkin artemredkin force-pushed the fix_bi_streaming branch 2 times, most recently from 4b3c27d to 06a4098 Compare March 3, 2021 12:40
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Seems reasonable enough to me, nothing obviously problematic in the code.

case .sendingBodyResponseHeadReceived:
self.state = .bodySentResponseHeadReceived
case .bodySentWaitingResponseHead, .bodySentResponseHeadReceived:
preconditionFailure("should not happen")
Copy link
Contributor

Choose a reason for hiding this comment

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

should we print self.state in that message?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

self.state = .bodySentResponseHeadReceived
case .bodySentWaitingResponseHead, .bodySentResponseHeadReceived:
preconditionFailure("should not happen")
case .endOrError, .redirected:
Copy link
Contributor

Choose a reason for hiding this comment

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

mind adding a comment why that's okay in .endOrError, or .redirected? I don't quite get how we can be in .end before we get the end of the body.

Copy link
Contributor

Choose a reason for hiding this comment

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

(similar for .redirected)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

rewrote, added a test

Motivation:
When we stream request body, current implementation expects that body
will finish streaming _before_ we start to receive response body parts.
This is not correct, reponse body parts can start to arrive before we
finish sending the request.

Modifications:
 - Simplifies state machine, we only case about request being fully sent
   to prevent sending body parts after .end, but response state machine
   is mostly ignored and correct flow will be handled by NIOHTTP1
   pipeline
 - Adds HTTPEchoHandler, that replies to each response body part
 - Adds bi-directional streaming test

Result:
Closes swift-server#327
Copy link
Contributor

@weissi weissi left a comment

Choose a reason for hiding this comment

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

very nice, thank you!

@weissi weissi merged commit 5d9b784 into swift-server:main Mar 3, 2021
@artemredkin artemredkin deleted the fix_bi_streaming branch March 3, 2021 18:38
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.

bi-directional streaming broken
3 participants