Skip to content

ChannelClosedException when server close connection right after releasing the last http2 stream #6258

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

huajiang-tubi
Copy link

@huajiang-tubi huajiang-tubi commented Jul 15, 2025

Motivation and Context

sequenceDiagram
    participant aws AS AWS Service
    participant channel AS Channel Event Loop
    participant pool AS HttpOrHttp2ChannelPool.eventLoop

    aws->>+channel: 1. LastHttpContent
    channel->>+channel: 2. ResponseHandler.finalizeResponse
    channel->>+aws: 3. Http2ResetFrame
    channel->>+channel: 4. HttpOrHttp2ChannelPool.release
    channel->>pool: 5. doInEventLoop(() -> release0(channel, promise))
    deactivate channel
    deactivate channel
    deactivate channel

    aws->>+channel: 6. Close The Connection
    channel->>+channel: 7. Http2ConnectionHandler.channelInactive
    channel->>+channel: 8. MultiplexedChannelRecord.closeAndExecuteOnChildChannels
    deactivate channel
    deactivate channel
    deactivate channel


    pool->>+pool: 9. HttpOrHttp2ChannelPool.release0(channel, promise)
    pool->>+channel: 10. doInEventLoop(() -> MultiplexedChannelRecord.closeAndReleaseChild)
    channel->>+channel: 11. childChannels.remove
    deactivate channel
    channel-->>-pool:
    deactivate pool
Loading
  1. The AWS service returns the final part of the response.

  2. ResponseHandler.finalizeResponse is invoked to complete processing of the response.

  3. The client sends an RST_STREAM frame to acknowledge the completion.

  4. It then calls release on the channel pool. Since the channel pool consists of multiple layers, this invocation eventually reaches HttpOrHttp2ChannelPool.release.

  5. HttpOrHttp2ChannelPool.release needs to access the protocolImpl field, which is only safely accessed from the pool’s event loop. To ensure thread safety, it submits a task to perform the release within that event loop.

  6. Meanwhile, the server receives the reset frame and decides to immediately close the connection (No idea why. It's a question for the service developer).

  7. This triggers channelInactive on the Http2ConnectionHandler.

  8. As a result, MultiplexedChannelRecord.closeAndExecuteOnChildChannels is called. It detects that there are still unreleased child channels because the task submitted in step 5 has not yet been executed. This leads to a ClosedChannelException being thrown and logged as an error.

  9. The release task is now running on the pool's event loop, but a little bit too late.

It may also be the root cause of #2914

Modifications

By declaring protocolImpl as volatile, we ensure visibility across threads once it is assigned within the pool's event loop. This guarantees that it can be safely accessed without thread-safety concerns. The underlying BetterFixedChannelPool is thread-safe, as its mutable state is managed exclusively within its dedicated event loop. For release operations, state updates are performed via a future listener after the underlying pool completes the release, effectively avoiding the concurrency issues observed with HttpOrHttp2ChannelPool.

Tests

Reproducing the issue consistently in a test environment is challenging, as it relies on the precise timing and order of event handler invocations.

We’ve been running the fix without the volatile keyword on protocolImpl in one of our production services for some time. It has been working well, as evidenced by a noticeable drop in error logs:
Screenshot 2025-07-16 at 21 15 01

The deployment with the volatile keyword added is now live. I’ll share an update on the results shortly.

@huajiang-tubi huajiang-tubi requested a review from a team as a code owner July 15, 2025 06:26
@huajiang-tubi huajiang-tubi changed the title ChannelClosedException when server close connection right after the last http2 stream ChannelClosedException when server close connection right after releasing the last http2 stream Jul 15, 2025
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.

1 participant