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

Fix idle HTTP/2 connection KeepAliveTimeout teardown #96157

Merged
merged 2 commits into from
Dec 19, 2023

Conversation

MihaZupan
Copy link
Member

Fixes #95621 for 9.0.
This was a regression introduced in 8.0 by #90094.


#95621 reports that the following exception was thrown in a timer callback, which in turn crashed the process:

System.Threading.Channels.ChannelClosedException: The channel has been closed.
at System.Net.Http.Http2Connection.FinalTeardown()
at System.Net.Http.Http2Connection.Shutdown()
at System.Net.Http.Http2Connection.Abort(Exception abortException)
at System.Net.Http.Http2Connection.HeartBeat()
at System.Net.Http.HttpConnectionPool.HeartBeat()
at System.Net.Http.HttpConnectionPoolManager.HeartBeat()
at System.Threading.TimerQueueTimer.Fire(Boolean isThreadPool)
at System.Threading.TimerQueue.FireNextTimers()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()

It is clear that FinalTeardown was somehow called twice.

Looking at the code involved in shutting down an HTTP/2 connection and removing unrelated things, it looks like this

bool _shutdown;

Abort()
{
    lock (SyncObject)
        Shutdown();
}

Dispose()
{
    lock (SyncObject)
        Shutdown();
}

Shutdown()
{
    if (!_shutdown)
    {
        InvalidateHttp2Connection(this);

        _shutdown = true;

        if (_streamsInUse == 0)
            FinalTeardown();
    }
}

InvalidateHttp2Connection(connection)
{
    if (TryRemoveConnectionFromPool())
        connection.Dispose();
}

// This should be called exactly once
void FinalTeardown() => _writeChannel.Writer.Complete();

Shutdown calls InvalidateHttp2Connection, which may call back into Dispose, which calls into Shutdown.
Because we set _shutdown after calling InvalidateHttp2Connection, we entered the if (!_shutdown) block twice (on the same call stack).
If the _streamsInUse == 0 condition holds, we will call into FinalTeardown twice.

This effectively means that if an idle HTTP/2 connection hits the KeepAliveTimeout, we'll crash the process.

The HttpKeepAlivePingPolicy_Always_NoPingResponseBetweenStreams_SecondRequestShouldFail test was meant to test this condition, but sadly it was always sending the second request on the client, so we weren't exercising the _streamsInUse == 0 condition.
After fixing the test, it is a 100 % repro for the reported issue.

The product change in this PR moves the toggling of the _shutdown flag before the call to InvalidateHttp2Connection.

@MihaZupan MihaZupan added this to the 9.0.0 milestone Dec 19, 2023
@MihaZupan MihaZupan requested review from CarnaViire, wfurt and a team December 19, 2023 00:28
@MihaZupan MihaZupan self-assigned this Dec 19, 2023
@ghost
Copy link

ghost commented Dec 19, 2023

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #95621 for 9.0.
This was a regression introduced in 8.0 by #90094.


#95621 reports that the following exception was thrown in a timer callback, which in turn crashed the process:

System.Threading.Channels.ChannelClosedException: The channel has been closed.
at System.Net.Http.Http2Connection.FinalTeardown()
at System.Net.Http.Http2Connection.Shutdown()
at System.Net.Http.Http2Connection.Abort(Exception abortException)
at System.Net.Http.Http2Connection.HeartBeat()
at System.Net.Http.HttpConnectionPool.HeartBeat()
at System.Net.Http.HttpConnectionPoolManager.HeartBeat()
at System.Threading.TimerQueueTimer.Fire(Boolean isThreadPool)
at System.Threading.TimerQueue.FireNextTimers()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()

It is clear that FinalTeardown was somehow called twice.

Looking at the code involved in shutting down an HTTP/2 connection and removing unrelated things, it looks like this

bool _shutdown;

Abort()
{
    lock (SyncObject)
        Shutdown();
}

Dispose()
{
    lock (SyncObject)
        Shutdown();
}

Shutdown()
{
    if (!_shutdown)
    {
        InvalidateHttp2Connection(this);

        _shutdown = true;

        if (_streamsInUse == 0)
            FinalTeardown();
    }
}

InvalidateHttp2Connection(connection)
{
    if (TryRemoveConnectionFromPool())
        connection.Dispose();
}

// This should be called exactly once
void FinalTeardown() => _writeChannel.Writer.Complete();

Shutdown calls InvalidateHttp2Connection, which may call back into Dispose, which calls into Shutdown.
Because we set _shutdown after calling InvalidateHttp2Connection, we entered the if (!_shutdown) block twice (on the same call stack).
If the _streamsInUse == 0 condition holds, we will call into FinalTeardown twice.

This effectively means that if an idle HTTP/2 connection hits the KeepAliveTimeout, we'll crash the process.

The HttpKeepAlivePingPolicy_Always_NoPingResponseBetweenStreams_SecondRequestShouldFail test was meant to test this condition, but sadly it was always sending the second request on the client, so we weren't exercising the _streamsInUse == 0 condition.
After fixing the test, it is a 100 % repro for the reported issue.

The product change in this PR moves the toggling of the _shutdown flag before the call to InvalidateHttp2Connection.

Author: MihaZupan
Assignees: MihaZupan
Labels:

area-System.Net.Http

Milestone: 9.0.0

Copy link
Member

@wfurt wfurt left a comment

Choose a reason for hiding this comment

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

LGTM

@MihaZupan
Copy link
Member Author

/azp run runtime-libraries-coreclr outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MihaZupan
Copy link
Member Author

/backport to release/8.0-staging

Copy link
Contributor

Started backporting to release/8.0-staging: https://github.com/dotnet/runtime/actions/runs/7256023935

@MihaZupan MihaZupan merged commit cb95281 into dotnet:main Dec 19, 2023
111 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Jan 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

.NET 8.0 ChannelClosedException using gRPC (Http2Connection)
2 participants