Skip to content

HttpClientFactory managed HttpClient deadlocks in single-threaded SynchronizationContext #999

Closed
@akunzai

Description

@akunzai

Describe the bug

my legacy app is still running on ASP.NET,
after internal library implementation adopting the HttpClientFactory managed HttpClient,
async call with await in HttpClient never returns.
this issue is very similar to #563

To Reproduce

dotnet new console -o Console1
cd Console1
dotnet add package Microsoft.Extensions.DependencyInjection --version 2.2.0
dotnet add package Microsoft.Extensions.Http --version 2.2.0

Program.cs

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;

namespace Console1
{
    public class Program
    {
        static void Main(string[] args)
        {
            var services = new ServiceCollection();
            services.AddHttpClient();
            var provider = services.BuildServiceProvider();
            var factory = provider.GetRequiredService<IHttpClientFactory>();
            var client1 = new HttpClient();
            SingleThreadedSynchronizationContext.Run(() =>
            {
                Console.Write("default HttpClient in single-threaded SynchronizationContext ... ");
                client1.GetAsync("http://example.com").GetAwaiter().GetResult();
                Console.WriteLine($"Done");
            });
            var client2 = factory.CreateClient();
            SingleThreadedSynchronizationContext.Run(() =>
            {
                Console.Write("managed HttpClient in single-threaded SynchronizationContext ... ");
                client2.GetAsync("http://example.com").GetAwaiter().GetResult();
                Console.WriteLine($"Done");
            });
        }
    }

    public class SingleThreadedSynchronizationContext : SynchronizationContext
    {
        private readonly Queue<(SendOrPostCallback Callback, object State)> _queue = new Queue<(SendOrPostCallback Callback, object State)>();

        public override void Post(SendOrPostCallback d, object state)
        {
            _queue.Enqueue((d, state));
        }

        public static void Run(Action action)
        {
            var previous = Current;
            var context = new SingleThreadedSynchronizationContext();
            SetSynchronizationContext(context);
            try
            {
                action();
                while (context._queue.TryDequeue(out var item))
                {
                    item.Callback(item.State);
                }
            }
            finally
            {
                SetSynchronizationContext(previous);
            }
        }
    }
}

Run this code

$ dotnet run
default HttpClient in single-threaded SynchronizationContext ... Done
managed HttpClient in single-threaded SynchronizationContext ...

you will see the second async call with await in HttpClient hangs

Expected behavior

HttpClientFactory managed HttpClient should also working in single-threaded SynchronizationContext

Screenshots

httpclient_deadlocks

Additional context

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   2.2.103
 Commit:    8edbc2570a

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.14
 OS Platform: Darwin
 RID:         osx.10.14-x64
 Base Path:   /usr/local/share/dotnet/sdk/2.2.103/

Host (useful for support):
  Version: 2.2.1
  Commit:  878dd11e62

.NET Core SDKs installed:
  2.2.103 [/usr/local/share/dotnet/sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.2.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.2.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.2.1 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

To install additional .NET Core runtimes or SDKs:
  https://aka.ms/dotnet-download

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions