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

Improve the cleanup of streaming responses #1119

Merged
merged 1 commit into from Apr 5, 2018

Conversation

trustin
Copy link
Collaborator

@trustin trustin commented Apr 4, 2018

Motivation:

A successfully sent response can be logged with AbortedStreamException
or ClosedChannelException due to race conditions.

Modifications:

  • Make HttpResponseSubscriber handle AbortedStreamException and
    ClosedChannelException better so that they are not logged
    unnecessarily
  • Make HttpServerHandler clean up unfinished HTTP/1 responses a little
    bit later so that they are not aborted too early.
  • Miscellaneous:
    • Fix a bug in DeferredStreamMessage where NeverInvokedSubscriber is
      invoked, which happens when a DeferredStreamMessage is delegated to
      another DeferredStreamMessage.

Result:

  • Much less chance of redundant AbortedStreamException or
    ClosedChannelException

@trustin trustin added the defect label Apr 4, 2018
@trustin trustin added this to the 0.61.0 milestone Apr 4, 2018
// One of the two cases:
// - Client closed the connection too early.
// - Response publisher aborted the stream.
failAndReset((AbortedStreamException) cause);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

/cc @kojilin You may want to send a canary to make sure this PR fixes your problem.

@@ -197,16 +197,41 @@ public int unfinishedRequests() {
}

@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
unfinishedResponses.keySet().forEach(StreamMessage::abort);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No idea why channelUnregistered is used instead of channelInactive but they don't make any difference, so I moved the cleanup logic to channelInactive.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The netty documentation on these callbacks wasn't very clear, so I implemented every method and added breakpoints to try to figure it out. This was called where I expected and unregistering from armeria when unregistering from netty made intuitive sense, but if they're similar semantics seems good.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

unregistration will always follow channelInactive. Netty allows deregistration without closing for a very special case, but we don't use it.

@@ -373,17 +398,15 @@ private void handleRequest(ChannelHandlerContext ctx, DecodedHttpRequest req) th
}
})).exceptionally(CompletionActions::log);

res.completionFuture().handle(voidFunction((ret, cause) -> {
res.completionFuture().handleAsync(voidFunction((ret, cause) -> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not really related with this PR, but thought this is cleaner.

if (data == null || !res.tryWrite(data)) {
break;
if (res.tryWrite(headers)) {
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.

/cc @kojilin This should fix the IllegalStateException issue you reported.

if (d == null || !res.tryWrite(d)) {
break;
if (res.tryWrite(headers)) {
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.

/cc @kojilin This should fix the IllegalStateException issue you reported.

@@ -97,6 +98,7 @@ public void jarBasedWebApp() throws Exception {
.startsWith("application/java");
assertThat(res.getFirstHeader(HttpHeaderNames.CONTENT_LENGTH.toString()).getValue())
.isEqualTo("1361");
EntityUtils.consume(res.getEntity());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Without this, Apache HC will close the connection before reading the response fully.

@@ -110,6 +112,7 @@ public void jarBasedWebAppWithAlternativeRoot() throws Exception {
.startsWith("application/java");
assertThat(res.getFirstHeader(HttpHeaderNames.CONTENT_LENGTH.toString()).getValue())
.isEqualTo("1361");
EntityUtils.consume(res.getEntity());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Without this, Apache HC will close the connection before reading the response fully.

case H1C:
case H1:
// XXX(trustin): How much time is 'a little bit'?
ctx.channel().eventLoop().schedule(this::cleanup, 1, TimeUnit.SECONDS);
Copy link
Collaborator Author

@trustin trustin Apr 4, 2018

Choose a reason for hiding this comment

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

I'm not sure if we should make this delay configurable. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Configuring just this knob is probably overkill - setting it based on idle timeout handler might make sense, but might also be overkill.

@codecov
Copy link

codecov bot commented Apr 4, 2018

Codecov Report

Merging #1119 into master will decrease coverage by 0.08%.
The diff coverage is 78.57%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #1119      +/-   ##
=========================================
- Coverage   72.48%   72.4%   -0.09%     
=========================================
  Files         510     510              
  Lines       22784   22801      +17     
  Branches     2826    2830       +4     
=========================================
- Hits        16514   16508       -6     
- Misses       4764    4782      +18     
- Partials     1506    1511       +5
Impacted Files Coverage Δ
...com/linecorp/armeria/server/HttpServerHandler.java 77.43% <100%> (-0.27%) ⬇️
...om/linecorp/armeria/server/jetty/JettyService.java 63.05% <25%> (-0.19%) ⬇️
.../linecorp/armeria/server/tomcat/TomcatService.java 62.13% <25%> (-4.26%) ⬇️
...p/armeria/common/stream/DeferredStreamMessage.java 87.06% <60%> (-0.32%) ⬇️
...inecorp/armeria/server/HttpResponseSubscriber.java 77.83% <92.59%> (+1.09%) ⬆️
...m/linecorp/armeria/internal/HttpObjectEncoder.java 58.33% <0%> (-20.84%) ⬇️
.../armeria/common/stream/NeverInvokedSubscriber.java 42.85% <0%> (-14.29%) ⬇️
...armeria/server/tomcat/Tomcat90ProtocolHandler.java 50% <0%> (-13.64%) ⬇️
...n/java/com/linecorp/armeria/server/HttpServer.java 40% <0%> (-10%) ⬇️
... and 9 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9ad1cae...02d282a. Read the comment docs.

@@ -197,16 +197,41 @@ public int unfinishedRequests() {
}

@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
unfinishedResponses.keySet().forEach(StreamMessage::abort);
Copy link
Collaborator

Choose a reason for hiding this comment

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

The netty documentation on these callbacks wasn't very clear, so I implemented every method and added breakpoints to try to figure it out. This was called where I expected and unregistering from armeria when unregistering from netty made intuitive sense, but if they're similar semantics seems good.

case H1C:
case H1:
// XXX(trustin): How much time is 'a little bit'?
ctx.channel().eventLoop().schedule(this::cleanup, 1, TimeUnit.SECONDS);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Configuring just this knob is probably overkill - setting it based on idle timeout handler might make sense, but might also be overkill.

if (responseEncoder != null) {
responseEncoder.close();
}

unfinishedResponses.keySet().forEach(res -> {
if (!res.isComplete()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be mistaken but I thought our semantics allowed aborting already completed streams (since completion is asynchronous I thought it's considered best effort vs other completion events)

Copy link
Collaborator Author

@trustin trustin Apr 4, 2018

Choose a reason for hiding this comment

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

Yeah, it's probably redundant. Let me run the tests again without this guard tomorrow and let you know.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Everything's fine without the guard. Removed.

cancelTimeout();
state = State.DONE;
final State state = this.state;
Copy link
Member

Choose a reason for hiding this comment

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

oldState to make it clear?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Motivation:

A successfully sent response can be logged with AbortedStreamException
or ClosedChannelException due to race conditions.

Modifications:

- Make HttpResponseSubscriber handle AbortedStreamException and
  ClosedChannelException better so that they are not logged
  unnecessarily
- Make HttpServerHandler clean up unfinished HTTP/1 responses a little
  bit later so that they are not aborted too early.
- Miscellaneous:
  - Fix a bug in DeferredStreamMessage where NeverInvokedSubscriber is
    invoked, which happens when a DeferredStreamMessage is delegated to
    another DeferredStreamMessage.

Result:

- Much less chance of redundant AbortedStreamException or
  ClosedChannelException
@trustin
Copy link
Collaborator Author

trustin commented Apr 5, 2018

Thanks for reviewing. Will merge once CI passes.

@trustin trustin merged commit de8dfff into line:master Apr 5, 2018
@trustin trustin deleted the fix_stream_cleanup branch April 5, 2018 06:56
@trustin
Copy link
Collaborator Author

trustin commented Apr 5, 2018

Thank you!

trustin added a commit that referenced this pull request Apr 5, 2018
Motivation:

A successfully sent response can be logged with AbortedStreamException
or ClosedChannelException due to race conditions.

Modifications:

- Make HttpResponseSubscriber handle AbortedStreamException and
  ClosedChannelException better so that they are not logged
  unnecessarily
- Make HttpServerHandler clean up unfinished HTTP/1 responses a little
  bit later so that they are not aborted too early.
- Miscellaneous:
  - Fix a bug in DeferredStreamMessage where NeverInvokedSubscriber is
    invoked, which happens when a DeferredStreamMessage is delegated to
    another DeferredStreamMessage.

Result:

- Much less chance of redundant AbortedStreamException or
  ClosedChannelException
fmguerreiro pushed a commit to fmguerreiro/armeria that referenced this pull request Sep 19, 2020
Motivation:

A successfully sent response can be logged with AbortedStreamException
or ClosedChannelException due to race conditions.

Modifications:

- Make HttpResponseSubscriber handle AbortedStreamException and
  ClosedChannelException better so that they are not logged
  unnecessarily
- Make HttpServerHandler clean up unfinished HTTP/1 responses a little
  bit later so that they are not aborted too early.
- Miscellaneous:
  - Fix a bug in DeferredStreamMessage where NeverInvokedSubscriber is
    invoked, which happens when a DeferredStreamMessage is delegated to
    another DeferredStreamMessage.

Result:

- Much less chance of redundant AbortedStreamException or
  ClosedChannelException
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants