I have read a lot about the socket exhaust problem. In general, this never caused me any problems, I always tried to follow the recommendations as much as possible. But now I have a question. I’m going to make a server application in ASP.net where the user will be able to dynamically instantiate and control instances of a MyClient
object which should have a unique set of HttpClient
with an HttpClientHandler
. It must be unique because certain CookieContainer
and Proxy
settings are used. Therefore, they will be created in Runtime, the settings are not known in advance.
MyClient
is a wrapper with a specific set of Http requests, which has a flexible and deep structure created for specific services. Essentially, it includes:
- Identity, thanks to the Cookie and Proxy set, which the user sets manually.
- State control, through an external API, the service determines the session state and other parameters from an external source.
- Requests to the API of an external service, allowing you to perform actions from the client.
- Dynamic life cycle. It is instantiated by the user and it is destroyed as it is no longer needed.
Each user has, although not infinite, but a set of MyClients from 1 to 1000+ instances that will work in parallel. With a life cycle from 5 minutes to months. For clarity, I will give an example code that does not reflect the whole essence, it only serves to understand the approximate implementation.
public class MyClient
{
public HttpClient Client { get; }
public HttpClientHandler Handler { get; }
public int ParameterOfExternalApi1 { get; private set; }
public object ParameterOfExternalApi2 { get; private set; }
public MyClient(CookieCollection cookies, IWebProxy proxy)
{
BuildClient(cookies, proxy);
//
}
private void BuildClient(CookieCollection cookies, IWebProxy proxy)
{
//Building client
Client = builtClient;
}
public async Task MyHttpRequestToExternalAPI()
{
await Client.GetAsync("uri");
}
public async Task<bool> CheckSession()
{
//
}
public void Dispose()
{
}
}
The lifecycle is managed through a pool of clients. If the client is idle, expired, or the user chooses to disable the client, the pool disposes of all unmanaged resources. Again, the code is very approximate, it serves only for clarity.
public static class MyPool
{
private static readonly ConcurrentDictionary<Guid, PooledClient> Pool = new();
public static MyClient? GetClient(Guid guid)
{
return Pool.TryGetValue(guid, out var pooled) ? pooled.Get() : null;
}
public static void Dispose(Guid guid)
{
//
}
}
public class PooledClient
{
private bool _disposed;
private readonly MyClient _client;
public PooledClient(MyClient client)
{
_client = client;
}
public MyClient? Get()
{
return _disposed ? null : _client;
}
public void Dispose()
{
//
}
}
Actually the question itself is: will not the use of so many HttpClients along with HttpClientHandler create problems, especially on 1000, 5000 or >50000 intances on one server? If it does, is there a solution or workaround?
upd
Same issue described in https://github.com/dotnet/runtime/issues/35992
Maybe it will clarify my requirenments to the request system.
2
Answers
That is insane. Seriously. This is the crucial mistake in the design.
The real solution is to not allow such situation. Because as it is, you will exhaust sockets, and there’s not much you can do about it. Except for changing the design.
First of all limit the number of client to dozens at most. Globally, not per user. Preferably just one, but depending on your concrete situation (custom TLS handling?) it might not be possible. Secondly lifetime of a month??? What exactly works so long that it needs such huge lifetime? Does your server run universe simulation? Even if, it is extremely unlikely that you need to keep an open network connection for such a long time.
If there is lots of work to do, then divide the data into pieces, save it to database and schedule workers (meaning process distinct from the server) to pick pieces one by one and process them. And then you can keep the number of clients low, and queued. And you have better control over distribution of tasks.
Also you don’t have couple cookie and proxy data with socket (wrappers). Why would you do that? Keep settings separated, and spawn http clients on demand. Put a limit on the number of http clients, and if someone tries to go above put him on a waiting queue.
Read IHttpClientFactory
Let the IHttpClientFactory manage each HttpClient’s lifetime