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

keep MsQuicConnection alive when streams are pending #52800

Merged
merged 9 commits into from
Jun 10, 2021

Conversation

wfurt
Copy link
Member

@wfurt wfurt commented May 15, 2021

This change has two parts:

When there are stream active we need to keep at least the native parts of MsQuick connection alive. This solves problem when dangling stream is disposed by finalizer as well as it covers cases when the connection is explicitly disposed before the stream(s).

This is somewhat similar to #52776 but different in details. Instead of exposing the internal state, it mimics what we do with connection and stream in H/2. Connection has stream counter and if there are pending stream we would defer cleanup of the native state. That would happen later when last stream goes.

Second part implements suggestion from @geoffkizer to flush the accept queue on connection disposal. When working on that I noticed that the stream may get lost if AcceptQueue.Writer.TryWrite fails (because for example it is already closed. I added logic to check and dispose the stream as well.

This change is focused on the relation between connection and stream and it does not touch live cycle of either one.
I will to the stream cleanup in follow up PR.

fixes #52048

@ghost
Copy link

ghost commented May 15, 2021

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

Issue Details

This change has two parts:

When there are stream active we need to keep at least the native parts of MsQuick connection alive. This solves problem when dangling stream is disposed by finalizer as well as it covers cases when the connection is explicitly disposed before the stream(s).

This is somewhat similar to #52776 but different in details. Instead of exposing the internal state, it mimics what we do with connection and stream in H/2. Connection has stream counter and if there are pending stream we would defer cleanup of the native state. That would happen later when last stream goes.

Second part implements suggestion from @geoffkizer to flush the accept queue on connection disposal. When working on that I noticed that the stream may get lost if AcceptQueue.Writer.TryWrite fails (because for example it is already closed. I added logic to check and dispose the stream as well.

This change is focused on the relation between connection and stream and it does not touch live cycle of either one.
I will to the stream cleanup in follow up PR.

fixes #52048

Author: wfurt
Assignees: wfurt
Labels:

area-System.Net.Quic

Milestone: -

@ManickaP
Copy link
Member

This is gonna clash with #52704, just FYI.

@wfurt
Copy link
Member Author

wfurt commented May 17, 2021

yes, I know @ManickaP. I started the crash investigation independently of what @CarnaViire was boring on.
I do think we will need some linkage between stream and connection for all this to work properly.
We can possibly play game with the SafeHandles and all the DangerousAddRef makes me worry

@wfurt
Copy link
Member Author

wfurt commented Jun 1, 2021

ping? Any brave soul for review?

ManagedAVE_MinimalFailingTest does not crash but it still sometimes fail with timeout. I think that is separate issue and I filed microsoft/msquic#1668 to investigate. It seems to be specific to calling close from finalizer thread.

Copy link
Member

@CarnaViire CarnaViire left a comment

Choose a reason for hiding this comment

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

LGTM

{
GCHandle gcHandle = GCHandle.FromIntPtr(_closingGCHandle);
Debug.Assert(gcHandle.IsAllocated);
if (gcHandle.IsAllocated) gcHandle.Free();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (gcHandle.IsAllocated) gcHandle.Free();
gcHandle.Free();

Copy link
Member Author

Choose a reason for hiding this comment

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

is this generally safe? We have other places with identical check.

Copy link
Member

Choose a reason for hiding this comment

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

IsAllocated just checks whether the GCHandle is not IntPtr.Zero. _closingGCHandle != IntPtr.Zero above does that already, so it should not be needed here.

@wfurt
Copy link
Member Author

wfurt commented Jun 3, 2021

The linked issue was resolved by moving Handle.Release() outside of lock block. It seems like finalizing the connection can flush some events and cause unexpected processing.

Copy link
Member

@ManickaP ManickaP left a comment

Choose a reason for hiding this comment

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

LGTM appart from one question.

@@ -43,6 +42,7 @@ internal sealed class MsQuicConnection : QuicConnectionProvider
internal sealed class State
{
public SafeMsQuicConnectionHandle Handle = null!; // set inside of MsQuicConnection ctor.
public GCHandle StateGCHandle;
Copy link
Member

Choose a reason for hiding this comment

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

I don't fully understand why was the StateGCHandle moved inside State?

Copy link
Member Author

Choose a reason for hiding this comment

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

When the connection is gone and state is kept alive because of the streams, we still want to lock state in place because we handed pointer to MsQuic. When we release all the streams, the original location is not accessible as we don't have link back to connection (and it may be gone) I did the move (sort of) in previous round using IntPtr. @CarnaViire and @jkotas suggested to preserve the type so I end up with this.

_state.AcceptQueue.Writer.Complete();
} catch { };

await foreach (MsQuicStream item in _state.AcceptQueue.Reader.ReadAllAsync())
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
await foreach (MsQuicStream item in _state.AcceptQueue.Reader.ReadAllAsync())
await foreach (MsQuicStream item in _state.AcceptQueue.Reader.ReadAllAsync().ConfigureAwait(false))

}
}

FlushAcceptQueue().GetAwaiter().GetResult();
Copy link
Contributor

Choose a reason for hiding this comment

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

Connections are IAsyncDisposable; we should make dispose do proper await 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.

I'm not sure what you suggest. We still need to flush for synchronous Dispose(). Can we do what ever needs to be done as follow-up? I would like to get this this in to get verifications on the crashes.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine.

@wfurt
Copy link
Member Author

wfurt commented Jun 10, 2021

Socket failure on macOS is unrelated.

@wfurt wfurt merged commit 68484ea into dotnet:main Jun 10, 2021
@wfurt wfurt deleted the streams_52048 branch June 10, 2021 15:49
@ghost ghost locked as resolved and limited conversation to collaborators Jul 10, 2021
@karelz karelz added this to the 6.0.0 milestone Jul 15, 2021
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.

[QUIC] Managed Access Violation in MsQuicStream finalizer
7 participants