Skip to content

Avoid leaking request contexts from server-side#6166

Merged
ikhoon merged 11 commits intoline:mainfrom
jrhee17:bugfix/server-req-leak
Apr 7, 2025
Merged

Avoid leaking request contexts from server-side#6166
ikhoon merged 11 commits intoline:mainfrom
jrhee17:bugfix/server-req-leak

Conversation

@jrhee17
Copy link
Contributor

@jrhee17 jrhee17 commented Mar 18, 2025

Motivation:

It has been pointed out that server-side may hold strong references to the ServiceRequestContext in #6108.

In general, a server-side HttpRequest may be closed either 1) by the client 2) or by the user.
When the request is closed by the client, the connection or stream state is reflected to the HttpRequest - hence the two are in sync.
However, if the HttpRequest is closed by the user the state is not always reflected to the connection or stream.
For instance, setting ServerBuilder#requestAutoAbortDelayMillis will close the request but not the underlying connection or stream.

The handling of ContentTooLargeException attempts to handle this issue. However, if setShouldResetOnlyIfRemoteIsOpen is set after the response is fully written, then the underlying stream may still never be closed.

Proposal
I propose the following:

  • HTTP1:
    • If there is a protocol failure, the Http1RequestDecoder is responsible for closing the connection
    • Otherwise, the underlying stream will be completed once the remote completes the request. Because pipelining is rarely used, HTTP1 connections will have a constraint of 1 concurrent request which probably won't impose a memory burden.
  • HTTP2:
    • Once the HttpRequest/HttpResponse is completed, the underlying stream state is checked. If the underlying stream is not closed yet, a RST_STREAM is sent to sync the stream state with the HttpRequest/HttpResponse state.
      • Note that this behavior relies on the fact that Http2Stream#State is updated synchronously when Http2FrameWriter#writeHeaders is called.

Modifications:

  • Introduced ServerHttp2ObjectEncoder#maybeResetStream which sends a RST_STREAM if the underlying state is not complete.
    • ServerHttp2ObjectEncoder#maybeResetStream is called when the protocol is HTTP2 and the request/response is complete.
  • Removed resetOnlyIfRemoteIsOpen since maybeResetStream replaces this functionality.

Result:

  • Users no longer observe memory pressure due to incomplete HTTP2 streams.

@jrhee17 jrhee17 marked this pull request as ready for review March 19, 2025 00:31
@jrhee17 jrhee17 added the defect label Mar 19, 2025
@jrhee17 jrhee17 added this to the 1.33.0 milestone Mar 19, 2025
Copy link
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

The new approach looks nice. 👍


if (responseEncoder instanceof ServerHttp2ObjectEncoder) {
((ServerHttp2ObjectEncoder) responseEncoder)
.maybeResetStream(req.streamId(), Http2Error.CANCEL);
Copy link
Contributor

Choose a reason for hiding this comment

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

question:
Can we do this only when needsDisconnection is false?

Copy link
Member

@trustin trustin left a comment

Choose a reason for hiding this comment

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

Simplicity 👍

@ikhoon ikhoon modified the milestones: 1.33.0, 1.32.4 Mar 31, 2025
Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

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

👍👍

@ikhoon ikhoon merged commit 50343cd into line:main Apr 7, 2025
12 of 14 checks passed
ikhoon pushed a commit to ikhoon/armeria that referenced this pull request Apr 8, 2025
Motivation:

It has been pointed out that server-side may hold strong references to
the `ServiceRequestContext` in
line#6108.

In general, a server-side `HttpRequest` may be closed either 1) by the
client 2) or by the user.
When the request is closed by the client, the connection or stream state
is reflected to the `HttpRequest` - hence the two are in sync.
However, if the `HttpRequest` is closed by the user the state is not
always reflected to the connection or stream.
For instance, setting `ServerBuilder#requestAutoAbortDelayMillis` will
close the request but not the underlying connection or stream.

The handling of `ContentTooLargeException` attempts to handle this
issue. However, if `setShouldResetOnlyIfRemoteIsOpen` is set after the
response is fully written, then the underlying stream may still never be
closed.

https://github.com/line/armeria/blob/6dbed576fe8f0774f445716c74c01c7572799ee6/core/src/main/java/com/linecorp/armeria/server/AbstractHttpResponseSubscriber.java#L389

**Proposal**
I propose the following:
- HTTP1:
- If there is a protocol failure, the `Http1RequestDecoder` is
responsible for closing the connection
- Otherwise, the underlying stream will be completed once the remote
completes the request. Because pipelining is rarely used, HTTP1
connections will have a constraint of 1 concurrent request which
probably won't impose a memory burden.
- HTTP2:
- Once the `HttpRequest`/`HttpResponse` is completed, the underlying
stream state is checked. If the underlying stream is not closed yet, a
`RST_STREAM` is sent to sync the stream state with the
`HttpRequest`/`HttpResponse` state.
- Note that this behavior relies on the fact that `Http2Stream#State` is
updated synchronously when `Http2FrameWriter#writeHeaders` is called.

Modifications:

- Introduced `ServerHttp2ObjectEncoder#maybeResetStream` which sends a
`RST_STREAM` if the underlying state is not complete.
- `ServerHttp2ObjectEncoder#maybeResetStream` is called when the
protocol is HTTP2 and the request/response is complete.
- Removed `resetOnlyIfRemoteIsOpen` since `maybeResetStream` replaces
this functionality.

Result:

- Users no longer observe memory pressure due to incomplete HTTP2
streams.
jrhee17 added a commit that referenced this pull request Jun 20, 2025
…6279)

Motivation:

The PR #6166 modified server-side behavior to send a `RST_STREAM` frame
when the corresponding Armeria's `HttpRequest` and `HttpResponse` are
completed. The assumption was that since the user was done using the
`HttpRequest` and `HttpResponse`, the underlying stream can be cleaned
up.


https://github.com/line/armeria/blob/a4bcb3e3b2a90a41b0066601d97da2fb04af5347/core/src/main/java/com/linecorp/armeria/server/ServerHttp2ObjectEncoder.java#L146-L149

The code was written to check Netty's `Http2Stream` before sending a
`RST_STREAM` - this is because sending a `endStream` would close the
underlying stream anyways regardless of whether a `RST_STREAM` is sent.

However, when frames are flow controlled, `Http2Stream`'s state is
updated after armeria's post-response future is invoked. To be more
specific, it seems like Netty's implementation updates the state by
adding a listener on the `write*` future when the flow controlled frame
is completed.
This is awkward because Armeria also completes the `HttpResponse` when a
write future with `endStream` is completed.

1. Armeria layer: Write `endStream` and wait on the write future before
invoking `res.close`


https://github.com/line/armeria/blob/a4bcb3e3b2a90a41b0066601d97da2fb04af5347/core/src/main/java/com/linecorp/armeria/server/AbstractHttpResponseSubscriber.java#L397-L417

2. `RequestAndResponseCompleteHandler` is completed


https://github.com/line/armeria/blob/a4bcb3e3b2a90a41b0066601d97da2fb04af5347/core/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java#L847-L848

4. `Http2Stream.state` is updated (the future is invoked after
Armeria's)


https://github.com/netty/netty/blob/a919dd3a53a50da080dc925a6fb085c81cab6294/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java#L629

Note that there is no issue when receiving requests since the state is
updated before the `Http2FrameListener` is invoked.

i.e.
https://github.com/netty/netty/blob/a919dd3a53a50da080dc925a6fb085c81cab6294/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java#L257

I propose that we track a separate state which determines whether an
`endStream` was sent. This value can be used to determine whether a
separate `RST_STREAM` should be sent to close the underlying stream.

Modifications:

- Added a `endStreamSentKey` property key which indicates whether an
`endStream` was sent for a `Http2Stream`.
- Check if `endStreamSentKey` was set when determining whether
`RST_STREAM` should be used to clean up the underlying stream.
- To determine if the local side (server->client) is open, both
`Http2Stream.State.localSideOpen() == true` and `endStreamSentKey ==
false` must be satisfied.
- If the remote is open (client->server), then the `RST_STREAM` is sent
regardless.
- For client-side, a late `RST_STREAM` is always logged if the response
is closed. If the stream is active, then the `RST_STREAM` was sent to
close the stream and hence there is no need to log with WARN level.
- Added `SimpleHttp2Connection` to easily send and receive HTTP2 frames
directly from test code

Result:

- An unecessary `RST_STREAM` is not sent after the server sends an
`endStream`.

<!--
Visit this URL to learn more about how to write a pull request
description:

https://armeria.dev/community/developer-guide#how-to-write-pull-request-description
-->
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.

4 participants

Comments