-
Notifications
You must be signed in to change notification settings - Fork 5k
Bugfix/113494 fix defaulthttpclientfactory memory leaks #114622
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
base: main
Are you sure you want to change the base?
Bugfix/113494 fix defaulthttpclientfactory memory leaks #114622
Conversation
…o separate namespace.
…egistrationTest" to separate file and methods.
…ers.Timer" for preventing unnecessary memory allocations.
@CarnaViire , can you review current PR? |
Can you please explain what "memory leak" are you referring to and how exactly your PR addresses it? As far as I'm aware, there is no memory leak associated with the timers. The only problem is that the timers outlive the ServiceProvider (due to |
If we'll add some additional async code to your code sample from here , like next: for (int i = 0; i < 10; i++)
{
var services = new ServiceCollection();
services.AddLogging(builder =>
{
builder.AddConsole().SetMinimumLevel(LogLevel.Trace);
});
services.AddScoped<DisposableService>();
services.AddTransient<TrackingHandler>();
services.AddHttpClient("TestClient")
.AddHttpMessageHandler<TrackingHandler>()
.RemoveAllLoggers()
.SetHandlerLifetime(TimeSpan.FromSeconds(1)); // Set a short lifetime to force handler to expire quickly.
using var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
Parallel.For(0, 1000, async (int i) => {
var client = httpClientFactory.CreateClient("TestClient");
Console.WriteLine($"Iteration {i + 1}: HttpClient acquired");
try
{
_ = await client.GetAsync("https://example.com");
}
catch (Exception)
{
// ignore
}
});
}
Console.WriteLine("Waiting for handlers to expire (lifetime = 1s)...");
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine("Forcing garbage collection... collecting HttpClients to enable expired handler cleanup");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC finished");
Console.WriteLine("Waiting for the cleanup timer to fire (every 10s)...");
await Task.Delay(TimeSpan.FromSeconds(15));
Console.WriteLine("15s wait completed");
// Output disposal statistics
DisposableService.PrintStats();
TrackingHandler.PrintStats(); we will get output like:
and comparison of memory snapshot will display us something like: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May I please ask you to revert all the refactorings that are not strictly necessary for the proposed functional change (both in the source code and the tests) -- this bloats up the PR, and blurs the value that it is supposed to introduce.
{ | ||
AutoReset = false | ||
}; | ||
_timer.Elapsed += (_, _) => Timer_Tick(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would unnecessary allocate each time an ActiveHandlerTrackingEntry is created.
In general, I am not sure the use of the heavy-weight System.Timers.Timer is justified here.
Thanks for the snippet above, I will review it and return to you a bit later |
@Maximys I've checked your example, and the reason behind this behavior is simply that not all HttpClients were GC'ed with that single GC.Collect. The cleanup is only possible when the handlers are not held by any of the clients anymore. I assume that the delay might be related to the load of the additional tasks scheduled and other Parallel.For machinery, so now it just takes a little bit more time than the arbitrary chosen wait interval. To make sure all the HttpClients are collected before we initiate the cleanup wait, we can e.g. track them with WeakRefs: var clients = new ConcurrentBag<WeakReference>();
for (int i = 0; i < 10; i++)
{
//...
Parallel.For(0, 1000, async i =>
{
var client = httpClientFactory.CreateClient("TestClient");
clients.Add(new WeakReference(client));
//...
});
}
//...
var aliveClients = clients.Where(wr => wr.IsAlive).ToList();
while (aliveClients.Count > 0)
{
Console.WriteLine($"Remaining HttpClients: {aliveClients.Count}");
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine("Forcing garbage collection... collecting HttpClients to enable expired handler cleanup");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC finished");
aliveClients = [.. aliveClients.Where(wr => wr.IsAlive)];
}
Console.WriteLine("Waiting for the cleanup timer to fire (every 10s)...");
//... and the output:
|
Current PR fix memory leaks, linked with requirements of System.Threading.Timer recreation, but it does not fix root couse of #113494 , which linked with scope creation by this line.
I think, the most applicable way for fix root couse is change of IHttpClientFactory , which should be represented by two method groups:
like it represented by ArrayPool. Then we can dispose created scope inside second method group.