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

Implement QUIC abort exceptions, fix channel usage, and implement some tests. #32051

Merged
merged 2 commits into from
Feb 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/libraries/System.Net.Quic/ref/System.Net.Quic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,8 @@ public class QuicStreamAbortedException : QuicException
public QuicStreamAbortedException(string message, long errorCode) : base(message) { }
public long ErrorCode { get; }
}
public class QuicOperationAbortedException : QuicException
Copy link
Member

@halter73 halter73 Feb 11, 2020

Choose a reason for hiding this comment

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

I see that QuicException derives directly from Exception. Would it be better to have it derive from IOException or possibly OperationCanceledException if it's always user-initiated?

I would lean towards IOException given how it's currently used. I see it's being triggered by stuff like "HandleEventShutdownInitiatedByPeer" which feels really IOExceptiony to me (if it needs to be an exception at all).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea that seems reasonable. Open up an issue for it?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also don't know if QuicOperationAbortedException will be necessary now if we start returning null.

Copy link
Member

Choose a reason for hiding this comment

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

I looked at SocketException, and it doesn't derive from IOException, so I'm no longer convinced this is the way to go. Maybe it should derive from SystemException (which SocketException does indirectly), but I'm not sure what the guidance there is. Is this supposed to be the base class for all Exceptions in a System.* namespace?

{
public QuicOperationAbortedException(string message) : base(message) { }
}
}
8 changes: 4 additions & 4 deletions src/libraries/System.Net.Quic/src/Interop/MsQuicEnums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal enum QUIC_STREAM_SHUTDOWN_FLAG : uint
}

[Flags]
internal enum QUIC_RECEIVE_FLAG : byte
internal enum QUIC_RECEIVE_FLAG : uint
{
NONE = 0,
ZERO_RTT = 0x1,
Expand Down Expand Up @@ -133,12 +133,12 @@ internal enum QUIC_PARAM_STREAM : uint
IDEAL_SEND_BUFFER = 2
}

internal enum QUIC_LISTENER_EVENT : byte
internal enum QUIC_LISTENER_EVENT : uint
{
NEW_CONNECTION = 0
}

internal enum QUIC_CONNECTION_EVENT : byte
internal enum QUIC_CONNECTION_EVENT : uint
{
CONNECTED = 0,
SHUTDOWN_INITIATED_BY_TRANSPORT = 1,
Expand All @@ -152,7 +152,7 @@ internal enum QUIC_CONNECTION_EVENT : byte
IDEAL_PROCESSOR_CHANGED = 9,
}

internal enum QUIC_STREAM_EVENT : byte
internal enum QUIC_STREAM_EVENT : uint
{
START_COMPLETE = 0,
RECEIVE = 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ internal struct ConnectionEventDataShutdownBegin
[StructLayout(LayoutKind.Sequential)]
internal struct ConnectionEventDataShutdownBeginPeer
{
internal ushort ErrorCode;
internal long ErrorCode;
}

[StructLayout(LayoutKind.Sequential)]
Expand Down Expand Up @@ -264,7 +264,7 @@ internal struct ConnectionEvent
internal bool EarlyDataAccepted => Data.Connected.EarlyDataAccepted;
internal ulong NumBytes => Data.IdealSendBuffer.NumBytes;
internal uint ShutdownBeginStatus => Data.ShutdownBegin.Status;
internal ushort ShutdownBeginPeerStatus => Data.ShutdownBeginPeer.ErrorCode;
internal long ShutdownBeginPeerStatus => Data.ShutdownBeginPeer.ErrorCode;
internal bool ShutdownTimedOut => Data.ShutdownComplete.TimedOut;
internal ushort BiDirectionalCount => Data.StreamsAvailable.BiDirectionalCount;
internal ushort UniDirectionalCount => Data.StreamsAvailable.UniDirectionalCount;
Expand Down Expand Up @@ -324,13 +324,13 @@ internal bool IsCanceled()
[StructLayout(LayoutKind.Sequential)]
internal struct StreamEventDataPeerSendAbort
{
internal ushort ErrorCode;
internal long ErrorCode;
}

[StructLayout(LayoutKind.Sequential)]
internal struct StreamEventDataPeerRecvAbort
{
internal ushort ErrorCode;
internal long ErrorCode;
}

[StructLayout(LayoutKind.Sequential)]
Expand Down
10 changes: 8 additions & 2 deletions src/libraries/System.Net.Quic/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,16 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="net_quic_connectionaborted" xml:space="preserve">
<value>Connection aborted by peer ({0}).</value>
</data>
<data name="net_quic_notsupported" xml:space="preserve">
<value>QUIC is not supported on this platform. See http://aka.ms/dotnetquic</value>
</data>
<data name="net_quic_placeholdertext" xml:space="preserve">
<value>Placeholder text</value>
<data name="net_quic_operationaborted" xml:space="preserve">
<value>Operation aborted.</value>
</data>
<data name="net_quic_streamaborted" xml:space="preserve">
<value>Stream aborted by peer ({0}).</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<Compile Include="System\Net\Quic\QuicConnection.cs" />
<Compile Include="System\Net\Quic\QuicListener.cs" />
<Compile Include="System\Net\Quic\QuicListenerOptions.cs" />
<Compile Include="System\Net\Quic\QuicOperationAbortedException.cs" />
<Compile Include="System\Net\Quic\QuicStream.cs" />
<Compile Include="System\Net\Quic\QuicException.cs" />
<Compile Include="System\Net\Quic\QuicConnectionAbortedException.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal sealed class MsQuicConnection : QuicConnectionProvider
private bool _disposed;
private bool _connected;
private MsQuicSecurityConfig _securityConfig;
private long _abortErrorCode = -1;

// Queue for accepted streams
private readonly Channel<MsQuicStream> _acceptQueue = Channel.CreateUnbounded<MsQuicStream>(new UnboundedChannelOptions()
Expand Down Expand Up @@ -200,6 +201,7 @@ private uint HandleEventShutdownInitiatedByTransport(ConnectionEvent connectionE

private uint HandleEventShutdownInitiatedByPeer(ConnectionEvent connectionEvent)
{
_abortErrorCode = connectionEvent.Data.ShutdownBeginPeer.ErrorCode;
_acceptQueue.Writer.Complete();
return MsQuicStatusCodes.Success;
}
Expand Down Expand Up @@ -237,18 +239,23 @@ internal override async ValueTask<QuicStreamProvider> AcceptStreamAsync(Cancella

ThrowIfDisposed();

if (await _acceptQueue.Reader.WaitToReadAsync(cancellationToken))
MsQuicStream stream;

try
{
stream = await _acceptQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
}
catch (ChannelClosedException)
{
if (_acceptQueue.Reader.TryRead(out MsQuicStream stream))
throw _abortErrorCode switch
{
if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
return stream;
}
-1 => new QuicOperationAbortedException(), // Shutdown initiated by us.
long err => new QuicConnectionAbortedException(err) // Shutdown initiated by peer.
};
}

if (NetEventSource.IsEnabled) NetEventSource.Exit(this);

return null;
Copy link
Member

Choose a reason for hiding this comment

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

I think it's better to continue returning null than to throw an exception from AcceptStreamAsync when the connection is closing. The connection being closed is a common enough that throwing an exception feels noisy. I know this will make the QuicConnection.AcceptStreamAsync() wrapper a little more complicated, but I think it's worth it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, looking back on this, I agree as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that anyone familiar with sockets will expect consistency with that, which is why I went that direction, but I also think most everyone else might expect it will return null. The question becomes who is the more likely user. It's trouble to merge Stream and Socket 🙃.

I don't feel strongly about it. We can change it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's keep it with the "current" behavior of returning null, but fix it in QuicConnection.AcceptStreamAsync.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I think this needs some more thought now. We should consider how consistent the APIs between QuicStream, QuicConnection, QuicListener need to be in how they behave on disposal. QuicStream.ReadAsync() etc. do not return a nullable type and will need to throw an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to commit this for now to unblock things (the APIs are currently broken, returning null triggers a bug) but will open an issue to figure this out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Issue: #32142

return stream;
}

internal override QuicStreamProvider OpenUnidirectionalStream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace System.Net.Quic.Implementations.MsQuic
{
internal class MsQuicListener : QuicListenerProvider, IDisposable
internal sealed class MsQuicListener : QuicListenerProvider, IDisposable
{
// Security configuration for MsQuic
private MsQuicSession _session;
Expand Down Expand Up @@ -65,21 +65,21 @@ internal override async ValueTask<QuicConnectionProvider> AcceptConnectionAsync(

ThrowIfDisposed();

if (await _acceptConnectionQueue.Reader.WaitToReadAsync())
{
if (_acceptConnectionQueue.Reader.TryRead(out MsQuicConnection connection))
{
// resolve security config here.
await connection.SetSecurityConfigForConnection(_sslOptions.ServerCertificate);
if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
MsQuicConnection connection;

return connection;
}
try
{
connection = await _acceptConnectionQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
}
catch (ChannelClosedException)
{
throw new QuicOperationAbortedException();
}

if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
await connection.SetSecurityConfigForConnection(_sslOptions.ServerCertificate);

return null;
Copy link
Member

Choose a reason for hiding this comment

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

I think we should continue returning null here too.

if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
return connection;
}

public override void Dispose()
Expand Down Expand Up @@ -174,7 +174,7 @@ internal unsafe uint ListenerCallbackHandler(
}
}

protected void StopAcceptingConnections()
private void StopAcceptingConnections()
{
_acceptConnectionQueue.Writer.TryComplete();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ internal sealed class MsQuicStream : QuicStreamProvider
private StartState _started;

private ReadState _readState;
private long _readErrorCode = -1;

private ShutdownWriteState _shutdownState;

private SendState _sendState;
private long _sendErrorCode = -1;

// Used by the class to indicate that the stream is m_Readable.
private readonly bool _canRead;
Expand Down Expand Up @@ -245,7 +247,11 @@ internal override async ValueTask<int> ReadAsync(Memory<byte> destination, Cance
}
else if (_readState == ReadState.Aborted)
{
throw new IOException("Reading has been aborted by the peer.");
throw _readErrorCode switch
{
-1 => new QuicOperationAbortedException(),
long err => new QuicStreamAbortedException(err)
};
}
}

Expand Down Expand Up @@ -402,6 +408,7 @@ internal override void Write(ReadOnlySpan<byte> buffer)
{
ThrowIfDisposed();

// TODO: optimize this.
WriteAsync(buffer.ToArray()).GetAwaiter().GetResult();
}

Expand Down Expand Up @@ -532,14 +539,13 @@ private uint HandleEvent(ref StreamEvent evt)
// Peer has told us to abort the reading side of the stream.
case QUIC_STREAM_EVENT.PEER_SEND_ABORTED:
{
status = HandleEventPeerSendAborted();
status = HandleEventPeerSendAborted(ref evt);
}
break;
// Peer has stopped receiving data, don't send anymore.
// Potentially throw when WriteAsync/FlushAsync.
case QUIC_STREAM_EVENT.PEER_RECEIVE_ABORTED:
{
status = HandleEventPeerRecvAbort();
status = HandleEventPeerRecvAborted(ref evt);
}
break;
// Occurs when shutdown is completed for the send side.
Expand Down Expand Up @@ -598,9 +604,26 @@ private unsafe uint HandleEventRecv(ref MsQuicNativeMethods.StreamEvent evt)
return MsQuicStatusCodes.Pending;
}

private uint HandleEventPeerRecvAbort()
private uint HandleEventPeerRecvAborted(ref StreamEvent evt)
{
if (NetEventSource.IsEnabled) NetEventSource.Enter(this);

bool shouldComplete = false;
lock (_sync)
{
if (_sendState == SendState.None)
{
shouldComplete = true;
}
_sendState = SendState.Aborted;
_sendErrorCode = evt.Data.PeerSendAbort.ErrorCode;
Copy link
Contributor

Choose a reason for hiding this comment

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

Make sendErrorCode a local variable. I don't believe it is used elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is used to throw a QuicStreamAbortedException on the other side of the ValueTask

}

if (shouldComplete)
{
_sendResettableCompletionSource.CompleteException(new QuicStreamAbortedException(_sendErrorCode));
}

if (NetEventSource.IsEnabled) NetEventSource.Exit(this);

return MsQuicStatusCodes.Success;
Expand Down Expand Up @@ -696,7 +719,7 @@ private uint HandleEventShutdownComplete()
return MsQuicStatusCodes.Success;
}

private uint HandleEventPeerSendAborted()
private uint HandleEventPeerSendAborted(ref StreamEvent evt)
{
if (NetEventSource.IsEnabled) NetEventSource.Enter(this);

Expand All @@ -708,11 +731,12 @@ private uint HandleEventPeerSendAborted()
shouldComplete = true;
}
_readState = ReadState.Aborted;
_readErrorCode = evt.Data.PeerSendAbort.ErrorCode;
}

if (shouldComplete)
{
_receiveResettableCompletionSource.CompleteException(new IOException("Reading has been aborted by the peer."));
_receiveResettableCompletionSource.CompleteException(new QuicStreamAbortedException(_readErrorCode));
}

if (NetEventSource.IsEnabled) NetEventSource.Exit(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ namespace System.Net.Quic
{
public class QuicConnectionAbortedException : QuicException
{
internal QuicConnectionAbortedException(long errorCode)
: this(SR.Format(SR.net_quic_connectionaborted, errorCode), errorCode)
{
}

public QuicConnectionAbortedException(string message, long errorCode)
: base (message)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace System.Net.Quic
{
public class QuicOperationAbortedException : QuicException
{
internal QuicOperationAbortedException()
: base(SR.net_quic_operationaborted)
{
}

public QuicOperationAbortedException(string message) : base(message)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ namespace System.Net.Quic
{
public class QuicStreamAbortedException : QuicException
{
internal QuicStreamAbortedException(long errorCode)
: this(SR.Format(SR.net_quic_streamaborted, errorCode), errorCode)
{
}

public QuicStreamAbortedException(string message, long errorCode)
: base (message)
: base(message)
{
ErrorCode = errorCode;
}
Expand Down
Loading