In-depth study of Dotnet Core IHttpClientFactory

Today, we take a deep dive into IHttpClientFactory.

I. Introduction

We first came into contact with it in Dotnet Framework HttpClient.

HttpClientProvides us with HTTPa basic way to interact. But this HttpClientwill throw us two big pits when it is used frequently. On the one hand, if we frequently create and release HttpClientinstances, it will cause the Socketsocket resources to be exhausted because Socketof the TIME_WAITtime after closing . This question does not expand, if you need to check TCPthe life cycle. On the other hand, if we create a HttpClientsingleton that is accessed when HTTPthe DNSrecording is changed, it will throw an exception, because HttpClientdoes not allow such a change.

Now, for this content, there is a better solution.

From the beginning Dotnet Core 2.1, the framework provides a new content: IHttpClientFactory.

IHttpClientFactoryUsed to create HTTPinteractive HttpClientinstances. It solves the two problems mentioned above by separating HttpClientthe management and the HttpMessageHandlerchain used to send content . Here, the important thing is to manage HttpClientHandlerthe life cycle of the pipeline terminal , and this is the actual connection handler.

In addition, IHttpClientFactoryit can also be used IHttpClientBuilderto easily customize HttpClientand content processing pipeline, create by pre-configuration HttpClient, or implemented as base address is provided to add HTTPclass operation.

Let's look at a simple example:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("WangPlus", c =>
    {
        c.BaseAddress = new Uri("https://github.com/humornif");
    })
    .ConfigureHttpClient(c =>
    {
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
    });
}

In this example, when you call ConfigureHttpClient()or AddHttpMessageHandler()to configure HttpClient, you are actually adding configuration to IOptionsthe instance HttpClientFactoryOptions. This method provides a lot of configuration options, you can go to the Microsoft documentation for details, not much to say here.

In class IHttpClientFactory, the same way are: creating a IHttpClientFactorysingleton instance, then call CreateClient(name)creates a name WangPlusof HttpClient.

Look at the following example:

public class MyService
{
    private readonly IHttpClientFactory _factory;
    public MyService(IHttpClientFactory factory)
    {
        _factory = factory;
    }
    public async Task DoSomething()
    {
        HttpClient client = _factory.CreateClient("WangPlus");
    }
}

The usage is very simple.

Below, we will CreateClient()conduct an analysis to understand IHttpClientFactorythe content behind.

Second, the creation process of HttpClient & HttpMessageHandler

CreateClient()Method is IHttpClientFactorythe main method of interaction.

Take a look at CreateClient()the code implementation:

private readonly IOptionsMonitor<HttpClientFactoryOptions> _optionsMonitor

public HttpClient CreateClient(string name)
{
    HttpMessageHandler handler = CreateHandler(name);
    var client = new HttpClient(handler, disposeHandler: false);

    HttpClientFactoryOptions options = _optionsMonitor.Get(name);
    for (int i = 0; i < options.HttpClientActions.Count; i++)
    {
        options.HttpClientActions[i](client);
    }

    return client;
}

The code looks very simple. First, CreateHandler()create a HttpMessageHandlerprocessing pipeline and pass in HttpClientthe name to be created .

With this processing pipeline, it can be created HttpClientand passed to the processing pipeline. It should be noted here that disposeHandler:falsethis parameter is used to ensure that when we release HttpClient, the processing management will not be released, because IHttpClientFactorythe processing of this pipeline will be completed by itself.

Then, IOptionsMonitorget the named client from the instance HttpClientFactoryOptions. It comes from Startup.ConfigureServices()the HttpClientconfiguration function added in and the BaseAddresssum and Headerother content are set.

Finally, it will be HttpClientreturned to the caller.

After understanding this content, let's take a look at the CreateHandler(name)method and study how the HttpMessageHandlerpipeline is created.

readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;;

readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory = (name) =>
    {
        return new Lazy<ActiveHandlerTrackingEntry>(() =>
        {
            return CreateHandlerEntry(name);
        }, LazyThreadSafetyMode.ExecutionAndPublication);
    };

public HttpMessageHandler CreateHandler(string name)
{
    ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;

    entry.StartExpiryTimer(_expiryCallback);

    return entry.Handler;
}

Look at this code: CreateHandler()do two things:

  1. Create or acquire ActiveHandlerTrackingEntry;

  2. Start a timer.

_activeHandlersIt is one ConcurrentDictionary<>, and HttpClientthe name stored in it (for example, in the code above WangPlus). The use here Lazy<>is a GetOrAdd()trick to keep the method thread safe. The actual creation of the processing pipeline is CreateHandlerEntryin, it creates one ActiveHandlerTrackingEntry.

ActiveHandlerTrackingEntryIt is an immutable object, contained HttpMessageHandlerand IServiceScopeinjected. In addition, it also contains an StartExpiryTimer()internal timer used with it to call the callback function when the timer expires.

Look at ActiveHandlerTrackingEntrythe definition:

internal class ActiveHandlerTrackingEntry
{
    public LifetimeTrackingHttpMessageHandler Handler { get; private set; }
    public TimeSpan Lifetime { get; }
    public string Name { get; }
    public IServiceScope Scope { get; }
    public void StartExpiryTimer(TimerCallback callback)
    {
        // Starts the internal timer
        // Executes the callback after Lifetime has expired.
        // If the timer has already started, is noop
    }
}

So the CreateHandlermethod either creates a new one ActiveHandlerTrackingEntry, or retrieves the entry from the dictionary, and then starts the timer.

In the next section, let's take a look at how CreateHandlerEntry()methods create ActiveHandlerTrackingEntryinstances.

Three, create and track HttpMessageHandler in CreateHandlerEntry

CreateHandlerEntryThe method is to create HttpClienta place for processing pipelines.

This part of the code is a bit complicated, let’s simplify it and focus on the research process:

private readonly IServiceProvider _services;

private readonly IHttpMessageHandlerBuilderFilter[] _filters;

private ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
    IServiceScope scope = _services.CreateScope(); 
    IServiceProvider services = scope.ServiceProvider;
    HttpClientFactoryOptions options = _optionsMonitor.Get(name);

    HttpMessageHandlerBuilder builder = services.GetRequiredService<HttpMessageHandlerBuilder>();
    builder.Name = name;

    Action<HttpMessageHandlerBuilder> configure = Configure;
    for (int i = _filters.Length - 1; i >= 0; i--)
    {
        configure = _filters[i].Configure(configure);
    }

    configure(builder);

    var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());

    return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);

    void Configure(HttpMessageHandlerBuilder b)
    {
        for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)
        {
            options.HttpMessageHandlerBuilderActions[i](b);
        }
    }
}

Create a container with root DI IServiceScope, from the associated IServiceProvideraccess to services associated, and then from HttpClientFactoryOptionsfind the corresponding names HttpClientand its configuration.

The next item to look up from the container is HttpMessageHandlerBuilderthat the default value is DefaultHttpMessageHandlerBuilderthat this value builds a processing pipeline by creating a main handler (responsible for establishing Socketsockets and sending requests HttpClientHandler). We can wrap this main handler by adding additional delegates to create custom management for requests and responses.

The additional delegation is DelegatingHandlerssimilar to Core's middleware pipeline:

  1. Configure()Startup.ConfigureServices()Build the DelegatingHandlerspipeline according to the provided configuration ;

  2. IHttpMessageHandlerBuilderFilterIt is IHttpClientFactorya filter injected into the constructor to add additional handlers to the delegated processing pipeline.

IHttpMessageHandlerBuilderFilterSimilar to the one IStartupFiltersregistered by default LoggingHttpMessageHandlerBuilderFilter. This filter adds two additional handlers to the delegate pipeline:

  1. At the beginning of the pipeline LoggingScopeHttpMessageHandler, a new log will be started Scope;

  2. At the end of the pipeline LoggingHttpMessageHandler, before the request is sent to the master HttpClientHandler, log the request and response;

Finally, the entire pipeline is packed in one LifetimeTrackingHttpMessageHandler. After the pipeline is processed, it will IServiceScopebe saved in a new ActiveHandlerTrackingEntryinstance along with the one used to create it , and given HttpClientFactoryOptionsthe lifetime defined in (the default is two minutes).

The entry is returned to the caller ( CreateHandler()method), added to the handler ConcurrentDictionary<>, added to the new HttpClientinstance (in the CreateClient()method), and returned to the original caller.

In the next lifetime (two minutes), whenever you call CreateClient(), you will get a new HttpClientinstance, but it has the same handler pipeline as when it was originally created.

Each named or typed HttpClienthas its own message handler pipeline. For example, the name WangPlustwo HttpClientinstances will have the same handler chain, but the name apiof HttpClientthe program chains have different processing.

In the next section, we will study the cleaning process after the timer expires.

Three, expired cleanup

With the default time, after two minutes, ActiveHandlerTrackingEntrythe timer stored in will expire and StartExpiryTimer()the callback method will be triggered ExpiryTimer_Tick().

ExpiryTimer_TickResponsible for ConcurrentDictionary<>deleting the handler record from the pool and adding it to the expired handler queue:

readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;

internal void ExpiryTimer_Tick(object state)
{
    var active = (ActiveHandlerTrackingEntry)state;

     _activeHandlers.TryRemove(active.Name, out Lazy<ActiveHandlerTrackingEntry> found);

    var expired = new ExpiredHandlerTrackingEntry(active);
    _expiredHandlers.Enqueue(expired);

    StartCleanupTimer();
}

When a handler _activeHandlersis deleted from the collection, when it is called CreateClient(), it will no longer HttpClientbe distributed with the new one , but will remain in memory until all HttpClientinstances referencing this handler are cleared, IHttpClientFactorythe handler will finally be released Program pipeline.

IHttpClientFactoryUse LifetimeTrackingHttpMessageHandlerand ExpiredHandlerTrackingEntryto track whether the handler is no longer referenced.

Look at the following code:

internal class ExpiredHandlerTrackingEntry
{
    private readonly WeakReference _livenessTracker;

    public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
    {
        Name = other.Name;
        Scope = other.Scope;

        _livenessTracker = new WeakReference(other.Handler);
        InnerHandler = other.Handler.InnerHandler;
    }

    public bool CanDispose => !_livenessTracker.IsAlive;

    public HttpMessageHandler InnerHandler { get; }
    public string Name { get; }
    public IServiceScope Scope { get; }
}

According to this code, a weak reference to ExpiredHandlerTrackingEntrythe pair LifetimeTrackingHttpMessageHandleris created . According to what was written in the previous section, it LifetimeTrackingHttpMessageHandleris the "outermost" handler in the pipeline, so it is a HttpClientdirectly referenced handler.

To LifetimeTrackingHttpMessageHandleruse WeakReferencemeans that the direct reference to the outermost handler in the pipeline is only in HttpClient. Once the garbage collector has collected all of these HttpClient, there LifetimeTrackingHttpMessageHandlerwill be no references and therefore will be released. ExpiredHandlerTrackingEntryCan be WeakReference.IsAlivedetected by.

After adding a record to the _expiredHandlersqueue, StartCleanupTimer()a timer will be started, which will fire after 10 seconds. The CleanupTimer_Tick()method is called after it is triggered to check whether all references to the handler have expired. If it is, the handler and IServiceScopewill be released. If not, they are added back to the queue and the cleanup timer starts again:

internal void CleanupTimer_Tick()
{
    StopCleanupTimer();

    int initialCount = _expiredHandlers.Count;
    for (int i = 0; i < initialCount; i++)
    {
        _expiredHandlers.TryDequeue(out ExpiredHandlerTrackingEntry entry);

        if (entry.CanDispose)
        {
            try
            {
                entry.InnerHandler.Dispose();
                entry.Scope?.Dispose();
            }
            catch (Exception ex)
            {
            }
        }
        else
        {
            _expiredHandlers.Enqueue(entry);
        }
    }

    if (_expiredHandlers.Count > 0)
    {
        StartCleanupTimer();
    }
}

In order to see the flow of the code, I made this code simple. There are also log records and thread lock related content in the original code.

This method is relatively simple: traverse the ExpiredHandlerTrackingEntryrecords and check whether LifetimeTrackingHttpMessageHandlerall references to the handler have been deleted . If there is, the handler and IServiceScopewill be released.

If there are still LifetimeTrackingHttpMessageHandleractive references to any handlers, put the entry back into the queue and start the cleanup timer again.

Four, summary

If you see here, it means you are still very patient.

This article is a study of the source code, which can help us understand IHttpClientFactoryhow it works, and how it fills the old HttpClientpit.

Sometimes it's helpful to look at the source code.

If you like it, come to a three-in-one, let more people benefit from you

Guess you like

Origin blog.csdn.net/sD7O95O/article/details/108898278