Search code examples
c#.nettcpdotnet-httpclienthttpclientfactory

When IHttpClientFactory close TCP connection internally?


based on the article https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

// in startup.cs
builder.Services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

// usage
public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;
    private readonly string _remoteServiceBaseUrl;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Catalog> GetCatalogItems(int page, int take, int? brand, int? type)
    {
        var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
                                                 page, take, brand, type);

        var responseString = await _httpClient.GetStringAsync(uri);  // <------------------

        var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
        return catalog;
    }
}

can I say after await _httpClient.GetStringAsync(uri):

A. The underlying TCP connection is not closed, no FIN packet has been sent to the server

B. After 5mins, the TCP connection will be closed, so FIN packet is sent to the server, but still there will be around 4 mins TIME_Wait period for it (twice the maximum segment lifetime which is 2mins by default), so the TCP connectio is actually closed after 9 mins?

is my understanding on A and B correct?


Solution

  • If you want to have more control about the underlying sockets' lifecycle then I would suggest to specify the PooledConnectionIdleTimeout and/or PooledConnectionLifetime of the SocketsHttpHandler:

    var handler = new SocketsHttpHandler
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(...),
        PooledConnectionIdleTimeout = TimeSpan.FromMinutes(...)
    };
    
    var client = new HttpClient(handler);
    

    UPDATE #1

    is my understanding on A and B correct?

    Using a long-living HttpClient is different then using HttpClientFactory with short-living HttpMessageHandlers.

    Your question is about the second case so, lets stick with that.

    If you don't specify the HandlerLifetime then it will use 2 minutes as a default value. If you set it explicitly like you did via the SetHandlerLifetime then it will use that TimeSpan as the activity tracking period.

    Inside the DefaultHttpClientFactory there is a method called CreateHandlerEntry which will call the following method:

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

    The ActiveHandlerTrackingEntry will use a simple timer to track the elapsed period without activity. It does not bother the handler directly.

    Rather it allows to the callers of the StartExpiryTimer method to specify a TimerCallback. The DefaultHttpClientFactory passes the ExpiryTimer_Tick delegate.

    This method simply removes the handler from the active handlers and moves that into the expired handlers queue (there might be outstanding/pending requests still).

    var active = (ActiveHandlerTrackingEntry)state!;
    bool removed = _activeHandlers.TryRemove(active.Name, out Lazy<ActiveHandlerTrackingEntry>? found);
    ...
    
    var expired = new ExpiredHandlerTrackingEntry(active);
    _expiredHandlers.Enqueue(expired);
    

    The CleanupTimer_Tick will take care of the expired handlers. It will Dispose the ActiveHandlerTrackingEntry's InnerHandler, which is a marker class called LifetimeTrackingHttpMessageHandler.

    Gladly that lengthy named handler receives the actual message handler as its constructor parameter. That received message handler is generated via a HttpMessageHandlerBuilder class. In case of the DefaultHttpClientFactory this will be the DefaultHttpMessageHandlerBuilder. That class uses the HttpClientHandler not the SocketsHttpHandler as its primary message handler.

    Depending on the TARGET_BROWSER the underlying handler will be either the BrowserHttpHandler or the SocketsHttpHandler

    #if TARGET_BROWSER
    using System.Diagnostics;
    using System.Net.Http.Metrics;
    using HttpHandlerType = System.Net.Http.BrowserHttpHandler;
    #else
    using HttpHandlerType = System.Net.Http.SocketsHttpHandler;
    #endif
    

    ...

    My point here (as it was said by others in the comments section) is that the HttpClientFactory is at a much higher abstraction level than the actual TCP messages. If you search for FIN or TIME_WAIT messages then you will only find the tcp_fsm.h.

    My advice here is that if you really care about the TCP messages then use TCPView to scrunatize the underlying communication. It might require several experiments to see how certain method calls changes the underlying connection lifecycle.