Skip to content

Conversation

@normanmaurer
Copy link
Member

… forward bytes left after removal.

Motivation:

We need to ensure we correctly guard against re-entrancy for all cases. Also we did not correctly ensure we forward pending data after removal which could lead to missing data after upgrades.

Modifications:

  • pause the parser when we need to callout to the pipeline and so ensure we never fire events throught the pipeline while in callbacks of http_parser
  • correctly forward any pending bytes if an upgrade happened when the decoder is removed
  • Ensure HTTPUpgradeHandler only remove decoder after the full request is received
  • Add testcase provided by @weissi

Result:

Correctly handle upgrades and pending data. Fixes #422.

@normanmaurer normanmaurer requested review from Lukasa and weissi May 22, 2018 15:48
@normanmaurer normanmaurer force-pushed the http_decoder branch 2 times, most recently from 930731a to 4855599 Compare May 22, 2018 15:49
}

public func handlerRemoved(ctx: ChannelHandlerContext) {
if let buffer = self.cumulationBuffer?.slice(), buffer.readableBytes > 0, self.parser.upgrade == 1, ctx.channel.isActive {
Copy link
Member

Choose a reason for hiding this comment

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

why not && ? Don't hugely mind but usually we use && I think

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we prefer && over , except when we need to separate conditional bindings.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also this is a really complex conditional, can we write a function to give this conditional a name?

public func handlerRemoved(ctx: ChannelHandlerContext) {
if let buffer = self.cumulationBuffer?.slice(), buffer.readableBytes > 0, self.parser.upgrade == 1, ctx.channel.isActive {
self.cumulationBuffer = nil
ctx.fireChannelRead(NIOAny(buffer))
Copy link
Member

Choose a reason for hiding this comment

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

is this really safe? It really feels like there's a scenario where the next channel hander expects HTTP messages and this gets invoked during a channel close and then the next handler crashes

Copy link
Member Author

Choose a reason for hiding this comment

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

Any better idea ? I think we currently don’t know better

Copy link
Member Author

Choose a reason for hiding this comment

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

Any better idea ? I think we currently don’t know better

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, don't do this if ctx.channel.isActive is false. That way if handlerRemoved is being invoked while the channel is being torn down you won't forward the data on.

Copy link
Member Author

Choose a reason for hiding this comment

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

I am confused... isn’t that exactly what I am doing here @Lukasa ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I legit didn't even see that. That's part of why I wanted this conditional factored out. ;)

self.cumulationBuffer = nil

if self.cumulationBuffer!.readableBytes == 0 {
// Its safe to just drop the cumulationBuffer as we not have any extra views into it that are represented as readerIndex / length.
Copy link
Member

Choose a reason for hiding this comment

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

... as we don't have any extra views

Copy link
Member

@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.

this looks really good actually! Any performance difference?


// Ensure we pause the parser after this callback is complete so we can safely callout
// to the pipeline.
c_nio_http_parser_pause(parser, 1)
Copy link
Member

Choose a reason for hiding this comment

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

don't we need similar stuff for on_body or so?

Copy link
Member

Choose a reason for hiding this comment

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

ignore me, we've got that

@normanmaurer
Copy link
Member Author

@weissi will run perf Tests after dinner

}

public func handlerRemoved(ctx: ChannelHandlerContext) {
if let buffer = self.cumulationBuffer?.slice(), buffer.readableBytes > 0, self.parser.upgrade == 1, ctx.channel.isActive {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we prefer && over , except when we need to separate conditional bindings.

}

public func handlerRemoved(ctx: ChannelHandlerContext) {
if let buffer = self.cumulationBuffer?.slice(), buffer.readableBytes > 0, self.parser.upgrade == 1, ctx.channel.isActive {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also this is a really complex conditional, can we write a function to give this conditional a name?

public func handlerRemoved(ctx: ChannelHandlerContext) {
if let buffer = self.cumulationBuffer?.slice(), buffer.readableBytes > 0, self.parser.upgrade == 1, ctx.channel.isActive {
self.cumulationBuffer = nil
ctx.fireChannelRead(NIOAny(buffer))
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, don't do this if ctx.channel.isActive is false. That way if handlerRemoved is being invoked while the channel is being torn down you won't forward the data on.

/// requests that we choose to punt on it entirely and not allow it. As it happens this is mostly fine:
/// the odds of someone needing to upgrade midway through the lifetime of a connection are very low.
public class HTTPServerUpgradeHandler: ChannelInboundHandler {

Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we adding this blank line?

Copy link
Member Author

Choose a reason for hiding this comment

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

Will revert, left-over from some debugging Code that was there

if bufferedMessages.count > 0 {
ctx.fireChannelReadComplete()

return {
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no need to heap allocate this, closing over the ChannelHandlerContext. Instead, factor this out to a separate function and just use a state variable.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did it this way as otherwise we will need to store multiple things (like request, response headers, upgrader) and I thought this code looks just nicer + it’s not expected to run so frequently. WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

we could try out luck with @convention(thin) ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Eh, I guess that's fine? It feels super weird to me, but ok. We can't use @convention(thin) because as @normanmaurer notes this actually closes over a bunch of state.

We could encapsulate this in a struct instead, to avoid the heap allocation.

Copy link
Member Author

Choose a reason for hiding this comment

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

I am happy with the struct as well I just thought this one is a bit nicer and it should not matter. That said I am fine either way, just tell me what to do :)

Copy link
Contributor

Choose a reason for hiding this comment

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

For now we can allocate the closure, we can always remove it later.

notUpgrading(ctx: ctx, data: data)
return
case .body(_):
// ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you mark this with a TODO that indicates that in a future NIO version we want to add an API where we deliver this to the upgrader in some form?

}
} else {
// We should only ever see a request header: by the time the body comes in we should
// be out of the pipeline. Anything else is an error.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment needs rewording as it's no longer accurate. Maybe:

We should decide if we're going to upgrade based on the first request header: if we aren't upgrading, by the time the body comes in we should be out of the pipeline. That means that if we don't think we're upgrading, the only thing we should see is a request head. Anything else in an error.

@weissi weissi added this to the 1.7.2 milestone May 22, 2018
@normanmaurer normanmaurer force-pushed the http_decoder branch 2 times, most recently from ae83e9c to 3051625 Compare May 22, 2018 18:55
@normanmaurer
Copy link
Member Author

normanmaurer commented May 23, 2018

Alright did run our perf-suite...

master:

measuring: no-net_http1_10k_reqs_1_conn: 0.193889021873474, 0.19300103187561, 0.189878940582275, 0.202571034431458, 0.195896029472351, 0.197288036346436, 0.202276945114136, 0.193497061729431, 0.199617028236389, 0.190865039825439,
measuring: http1_10k_reqs_1_conn: 0.559623003005981, 0.557006001472473, 0.561303019523621, 0.561969041824341, 0.565063953399658, 0.564036011695862, 0.570590972900391, 0.555812001228333, 0.560003042221069, 0.556936979293823,
measuring: http1_10k_reqs_100_conns: 0.602265000343323, 0.603261947631836, 0.60293710231781, 0.606209993362427, 0.620349049568176, 0.612009048461914, 0.66516900062561, 0.610424995422363, 0.606754899024963, 0.618997097015381,

With this pr:

measuring: no-net_http1_10k_reqs_1_conn: 0.231523990631104, 0.221113085746765, 0.226511001586914, 0.227283000946045, 0.221638917922974, 0.222470045089722, 0.224020957946777, 0.230138063430786, 0.223469018936157, 0.220298051834106,
measuring: http1_10k_reqs_1_conn: 0.599761009216309, 0.598889946937561, 0.595099091529846, 0.598055958747864, 0.602496981620789, 0.640310049057007, 0.588855981826782, 0.594573974609375, 0.610931038856506, 0.599555015563965,
measuring: http1_10k_reqs_100_conns: 0.64155101776123, 0.639289975166321, 0.645186901092529, 0.649052023887634, 0.64255690574646, 0.644281029701233, 0.637521982192993, 0.642242074012756, 0.65774393081665, 0.642369031906128,

@normanmaurer
Copy link
Member Author

So it seems like this definitely harm the perf a little bit, which I think is not really unexpected with the extra branching etc.

@normanmaurer
Copy link
Member Author

@weissi @Lukasa ^^

@weissi
Copy link
Member

weissi commented May 23, 2018

@normanmaurer ouch, raw performance, no networking (no-net_http1_10k_reqs_1_conn) down 16% 😢.

@normanmaurer
Copy link
Member Author

@weissi yeah... that said I am not sure there is much we can do here (except use a pending list again)... I am open to suggestions tho :)

self.state.currentError = HTTPParserError.httpError(fromCHTTPParserErrno: http_errno(rawValue: httpError))!
ctx.fireErrorCaught(self.state.currentError!)
// Also take into account that we may have called c_nio_http_parser_pause(...)
guard httpError != 0 && httpError != HPE_PAUSED.rawValue else {
Copy link
Member Author

Choose a reason for hiding this comment

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

@weissi I guess it not really matters much for perf stuff but we may want to consider switching these as most of the time it will be HPE_PAUSED in reality so the first test is a waste mostly.

Copy link
Member Author

Choose a reason for hiding this comment

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

reordered this one

@weissi
Copy link
Member

weissi commented May 23, 2018

@normanmaurer is it faster with the callout list?

@normanmaurer
Copy link
Member Author

@weissi I will check in a few need to fix some other stuff first :(

@normanmaurer
Copy link
Member Author

After 61b3149 :

measuring: no-net_http1_10k_reqs_1_conn: 0.2206209897995, 0.221046090126038, 0.211659908294678, 0.212454080581665, 0.210615038871765, 0.208593010902405, 0.209282994270325, 0.208821058273315, 0.209628939628601, 0.209208965301514,
measuring: http1_10k_reqs_1_conn: 0.580152988433838, 0.57462203502655, 0.577759981155396, 0.582140922546387, 0.578655004501343, 0.572811961174011, 0.572220087051392, 0.578151941299438, 0.583968043327332, 0.611997961997986,
measuring: http1_10k_reqs_100_conns: 0.62391996383667, 0.626338005065918, 0.628537058830261, 0.629424929618835, 0.61939001083374, 0.638810038566589, 0.632416963577271, 0.63428795337677, 0.626121044158936, 0.628298044204712,

So not too bad imho @weissi

@weissi
Copy link
Member

weissi commented May 23, 2018

@normanmaurer much better, 8%. I'm probably happy enough with this

@normanmaurer
Copy link
Member Author

@weissi you want me to try a list as well still ?

@weissi
Copy link
Member

weissi commented May 23, 2018

@normanmaurer up to you but I think it could be a follow-up. Because this fixes a correctness issue so I think we should get it in asap

guard self.parser.upgrade == 1 && ctx.channel.isActive else {
return nil
}
if let buffer = self.cumulationBuffer?.slice(), buffer.readableBytes > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of interest, why are we slicing the cumulation buffer here?

Copy link
Member Author

Choose a reason for hiding this comment

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

@Lukasa I just thought it may be a bit nicer to consume. That said we not need to slice it. Happy to remove if you think we should

Copy link
Contributor

Choose a reason for hiding this comment

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

Unexplained slicing without a code comment is the kind of thing that we'll be real nervous to change in 6 months time. :D I think either remove it or put a comment explaining why it's there.

Copy link
Member Author

Choose a reason for hiding this comment

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

added a comment ;)

@normanmaurer
Copy link
Member Author

@weissi sounds good to me... waiting for @Lukasa then :) Also please don't forget to approve ;)

Copy link
Contributor

@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.

WFM.

@Lukasa Lukasa added the 🔨 semver/patch No public API change. label May 23, 2018
… forward bytes left after removal.

Motivation:

We need to ensure we correctly guard against re-entrancy for all cases. Also we did not correctly ensure we forward pending data after removal which could lead to missing data after upgrades.

Modifications:

- pause the parser when we need to callout to the pipeline and so ensure we never fire events throught the pipeline while in callbacks of http_parser
- correctly forward any pending bytes if an upgrade happened when the decoder is removed
- Ensure HTTPUpgradeHandler only remove decoder after the full request is received
- Add testcase provided by @weissi

Result:

Correctly handle upgrades and pending data. Fixes apple#422.
@normanmaurer
Copy link
Member Author

I squashed, rebased and pushed again

Copy link
Member

@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.

thanks, looks great!

@weissi weissi merged commit 398b950 into apple:master May 23, 2018
@normanmaurer normanmaurer deleted the http_decoder branch May 23, 2018 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants