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

UnobservedTaskException from QuicConnection that has not been disposed #112094

Open
WANDOKING opened this issue Feb 3, 2025 · 5 comments · May be fixed by #112190
Open

UnobservedTaskException from QuicConnection that has not been disposed #112094

WANDOKING opened this issue Feb 3, 2025 · 5 comments · May be fixed by #112190
Assignees
Labels
area-System.Net.Quic bug in-pr There is an active PR which will close this issue when it is merged
Milestone

Comments

@WANDOKING
Copy link

WANDOKING commented Feb 3, 2025

Description

I am developing a simple echo server application using System.Net.Quic.

After closing QuicConnection, if I do not Dispose, the following log is observed intermittently.

2025-02-03 20:16:08.8433	FATAL	EchoBot.BotRunner	Unobserved Task Exception: System.Net.Quic.QuicException: Operation aborted.
   at System.Net.Quic.QuicConnection.HandleEventShutdownComplete()
   at System.Net.Quic.QuicConnection.HandleConnectionEvent(QUIC_CONNECTION_EVENT& connectionEvent)
   at System.Net.Quic.QuicConnection.NativeCallback(QUIC_HANDLE* connection, Void* context, QUIC_CONNECTION_EVENT* connectionEvent)
--- End of stack trace from previous location ---

After changing the code to call DisposeAsync instead of CloseAsync on QuicConnection, the problem seems to have disappeared, but I am reporting this because I think this is a problematic behavior.

I think it might have to do with the following code related to connectedTcs:

    private unsafe int HandleEventShutdownComplete()
    {
        // make sure we log at least some secrets in case of shutdown before handshake completes.
        _tlsSecret?.WriteSecret();
 
        Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(_disposed ? new ObjectDisposedException(GetType().FullName) : ThrowHelper.GetOperationAbortedException());
        _connectionCloseTcs.TrySetException(exception);
        _acceptQueue.Writer.TryComplete(exception);
        _connectedTcs.TrySetException(exception);
        _shutdownTokenSource.Cancel();
        _shutdownTcs.TrySetResult(final: true);
        return QUIC_STATUS_SUCCESS;
    }

Reproduction Steps

Open a bidirectional stream with QuicConnection on the client side, and have a Read loop in one thread and a Write loop in another thread.
Client intermittently closes the Connection.

Expected behavior

Internally handles QuicException.

Actual behavior

If I don't call Dispose(), an UnobservedTaskException occurs.

Regression?

No response

Known Workarounds

No response

Configuration

  • .NET version: 9.0.101
  • OS: Windows 11
  • Architecture: x64

Other information

Looks like it's related to #80111

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Feb 3, 2025
@MihaZupan
Copy link
Member

Looks like it's related to #80111

Yes, unfrotunately we haven't fully fixed all cases yet.
Can also be seen in #111126

@MihaZupan MihaZupan added the bug label Feb 3, 2025
@ManickaP
Copy link
Member

ManickaP commented Feb 3, 2025

Can you share the exact repro code instead of code description please.

Also just FYI you should always dispose the connection, even if you call close async.

@ManickaP ManickaP added the needs-author-action An issue or pull request that requires more info or actions from the author. label Feb 3, 2025
@WANDOKING
Copy link
Author

Here is the code to reproduce the problem I was having. (It's not a good code, but I wrote it the way I remembered it.)

// Server
using System.Buffers;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace Server;

internal class Server
{
    private static async Task Main(string[] args)
    {
        TaskScheduler.UnobservedTaskException += (sender, e) =>
        {
            if (e.Exception is AggregateException ex)
            {
                foreach (Exception exception in ex.InnerExceptions)
                {
                    Console.WriteLine(exception);
                }
            }
        };

        var serverConnectionOptions = new QuicServerConnectionOptions()
        {
            DefaultStreamErrorCode = 0x0A,
            DefaultCloseErrorCode = 0x0B,
            IdleTimeout = TimeSpan.FromSeconds(60),
            ServerAuthenticationOptions = new SslServerAuthenticationOptions
            {
                ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
                ServerCertificate = X509CertificateLoader.LoadPkcs12FromFile(@"server.pfx", "test"),
            }
        };

        var quicListenerOptions = new QuicListenerOptions
        {
            ListenEndPoint = new IPEndPoint(IPAddress.Any, 23456),
            ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
            ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(serverConnectionOptions),
        };

        await using QuicListener listener = await QuicListener.ListenAsync(quicListenerOptions);

        int clientId = 0;
        while (true)
        {
            QuicConnection connection = await listener.AcceptConnectionAsync();
            //Console.WriteLine($"connection = {connection.RemoteEndPoint} | {connection.NegotiatedApplicationProtocol}");
            _ = StartReceive(++clientId, connection);
        }
    }

    private static async Task StartReceive(int clientId, QuicConnection client)
    {
        byte[] receiveBuffer = ArrayPool<byte>.Shared.Rent(4096);

        try
        {
            await using var stream = await client.AcceptInboundStreamAsync().ConfigureAwait(false);

            while (true)
            {
                int receivedBytesCount = await stream.ReadAsync(receiveBuffer).ConfigureAwait(false);

                //Console.WriteLine($"Received Count = {receivedBytesCount}, Data = {Encoding.UTF8.GetString(receiveBuffer)}");

                // Echo for test
                await stream.WriteAsync(receiveBuffer, 0, receivedBytesCount).ConfigureAwait(false);
            }
        }
        catch (Exception ex)
        {
            //Console.WriteLine($"ID {clientId} : {ex}");
        }
        finally
        {
            Console.WriteLine($"ID {clientId} | Disconnected.");

            // I know calling DisposeAsync in this part is the correct code,
            // but I'm wondering if it's the intended behavior
            // to throw an UnobservedTaskException because I missed calling DisposeAsync.
            await client.CloseAsync(0x0B).ConfigureAwait(false);

            ArrayPool<byte>.Shared.Return(receiveBuffer);
        }
    }
}

// Client

using System.Net.Quic;
using System.Net.Security;
using System.Net;
using System.Buffers;
using System.Text;

namespace Client;

internal class Client
{
    private static async Task Main(string[] args)
    {
        TaskScheduler.UnobservedTaskException += (sender, e) =>
        {
            Console.WriteLine(e);
        };

        await Task.Delay(1000);

        var clientConnectionOptions = new QuicClientConnectionOptions()
        {
            RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 23456),

            DefaultStreamErrorCode = 0x0A,
            DefaultCloseErrorCode = 0x0B,

            MaxInboundUnidirectionalStreams = 0,
            MaxInboundBidirectionalStreams = 1,

            ClientAuthenticationOptions = new SslClientAuthenticationOptions
            {
                ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
                RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true,
            }
        };

        for (int i = 0; i < 1000; ++i)
        {
            _ = ClientLoop(clientConnectionOptions);
            await Task.Delay(5);
        }

        await Task.Delay(Timeout.Infinite);
    }

    private static async Task ClientLoop(QuicClientConnectionOptions options)
    {
        QuicConnection connection = await QuicConnection.ConnectAsync(options);
        Console.WriteLine($"Connected {connection.LocalEndPoint} --> {connection.RemoteEndPoint}");

        byte[] receiveBuffer = ArrayPool<byte>.Shared.Rent(1024);
        int number = 0;
        using var stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
        while (true)
        {
            byte[] helloWorldToBytes = Encoding.UTF8.GetBytes($"Hello, World! ({number++})");

            if (Random.Shared.Next() % 10 == 0)
            {
                await connection.CloseAsync(0x0B);
                return;
            }

            await stream.WriteAsync(helloWorldToBytes, 0, helloWorldToBytes.Length);
            await stream.ReadExactlyAsync(receiveBuffer, 0, helloWorldToBytes.Length);
            string receivedString = Encoding.UTF8.GetString(receiveBuffer);

            Console.WriteLine(receivedString);

            await Task.Delay(10);
        }
    }
}

@dotnet-policy-service dotnet-policy-service bot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Feb 3, 2025
@ManickaP
Copy link
Member

ManickaP commented Feb 5, 2025

Can also be seen in #111126

I don't see this particular error in the log shared by Anton there.

@MihaZupan
Copy link
Member

I meant the general "unobserved exceptions from System.Net.Quic" ala #80111, I didn't check for the specific stack.

@ManickaP ManickaP self-assigned this Feb 5, 2025
@ManickaP ManickaP removed the untriaged New issue has not been triaged by the area owner label Feb 5, 2025
@ManickaP ManickaP added this to the 10.0.0 milestone Feb 5, 2025
@ManickaP ManickaP linked a pull request Feb 5, 2025 that will close this issue
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Net.Quic bug in-pr There is an active PR which will close this issue when it is merged
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants