Skip to content

Make HttpClientFactory implement IDisposable to clean up resources #115826

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

Draft
wants to merge 49 commits into
base: main
Choose a base branch
from

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented May 21, 2025

Issue Description

When creating and disposing service providers that use HttpClientFactory, there's a memory leak because timers aren't properly cleaned up. This particularly affects scenarios where service providers are frequently created and disposed, such as in test environments.

The problem occurs because:

  1. Active handler tracking entries create timers that aren't disposed when the service provider is disposed
  2. The cleanup timer continues running in the background
  3. These timers prevent proper garbage collection of resources

Changes Made

  1. Made DefaultHttpClientFactory implement IDisposable interface
  2. Added StopTimer() method to ActiveHandlerTrackingEntry to stop and dispose timers
  3. Implemented Dispose() method in DefaultHttpClientFactory that:
    • Stops the cleanup timer
    • Stops all active handler timers
    • Immediately disposes all expired handlers regardless of their CanDispose status
    • Disposes all active handlers and their associated resources
    • Clears collections to help with garbage collection
  4. Added a unit test that verifies proper resource cleanup when a service provider is disposed

Benefits

  • Properly cleans up resources when a service provider is disposed
  • Prevents memory leaks in applications that create and dispose service providers frequently
  • No behavioral changes for existing code that doesn't dispose service providers
  • Guarantees cleanup without requiring garbage collection

Fixes #113494.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • pkgs.dev.azure.com
    • Triggering command: /home/REDACTED/work/runtime/runtime/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/work/runtime/runtime/artifacts/toolset/restore.proj /t:__WriteToolsetLocation /clp:ErrorsOnly;NoSummary /p:__ToolsetLocationOutputFile=/home/REDACTED/work/runtime/runtime/artifacts/toolset/10.0.0-beta.25260.104.txt (dns block)
    • Triggering command: ./.dotnet/dotnet build src/libraries/Microsoft.Extensions.Http/src/Microsoft.Extensions.Http.csproj (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@dotnet-policy-service dotnet-policy-service bot added the linkable-framework Issues associated with delivering a linker friendly framework label May 21, 2025
Copilot AI and others added 2 commits May 21, 2025 12:20
Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
@Copilot Copilot AI changed the title [WIP] HttpClientFactory doesn't clean up after itself Make HttpClientFactory implement IDisposable to clean up resources May 21, 2025
@Copilot Copilot AI requested a review from CarnaViire May 21, 2025 12:24
Copilot finished work on behalf of CarnaViire May 21, 2025 12:25
@CarnaViire CarnaViire added area-Extensions-HttpClientFactory and removed linkable-framework Issues associated with delivering a linker friendly framework labels May 21, 2025
Copy link
Contributor

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

…actory.cs

Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Copilot finished work on behalf of CarnaViire May 21, 2025 17:33
Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Copilot finished work on behalf of CarnaViire May 21, 2025 18:27
@CarnaViire
Copy link
Member

@copilot let's try again

…logic

Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented May 23, 2025

@copilot let's try again

I've addressed all the code review feedback in commit 9c60237:

  1. Simplified the condition check in ActiveHandlerTrackingEntry.Dispose() to use != 0 directly
  2. Removed the redundant IsValueCreated check and used a variable for entry.Value.Value in DefaultHttpClientFactory
  3. Restructured ExpiredHandlerTrackingEntry.Dispose() to ensure handler and scope are disposed together with proper try-finally blocks

The code builds successfully and is ready for further review.

Copilot finished work on behalf of CarnaViire May 23, 2025 21:28
…dler is still alive

Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Copilot finished work on behalf of CarnaViire May 23, 2025 22:43
@CarnaViire

This comment was marked as resolved.

Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented May 23, 2025

@copilot there was a test error reported in CI:

...

Fixed in commit b0216e5. The test was failing because all clients were being created with the same name "test-client", which causes the factory to reuse the same handler for all clients. I've updated the test to create clients with unique names, ensuring we get 5 distinct handlers that are properly tracked and disposed.

Copilot finished work on behalf of CarnaViire May 23, 2025 23:47
…logic

Co-authored-by: CarnaViire <3184057+CarnaViire@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented May 27, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of CarnaViire May 27, 2025 17:25
private const int ClientCount = 5;

[Fact]
public async Task DisposingServiceProvider_DisposesHttpClientFactory_ReleasesResources()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Let's test disposing of the scope as well. This can be done by making DisposeTrackingHandler depend on some scoped service, that is also IDisposable; and then track/count these services in a similar way.
  2. We lost testing the expired handlers at some point, too. You should use the test factory modification from
    private class TestHttpClientFactory : DefaultHttpClientFactory
    {
    public TestHttpClientFactory(
    IServiceProvider services,
    IServiceScopeFactory scopeFactory,
    IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor,
    IEnumerable<IHttpMessageHandlerBuilderFilter> filters)
    : base(services, scopeFactory, optionsMonitor, filters)
    {
    ActiveEntryState = new Dictionary<ActiveHandlerTrackingEntry, (TaskCompletionSource<ActiveHandlerTrackingEntry>, Task)>();
    CleanupTimerStarted = new ManualResetEventSlim(initialState: false);
    }
    public bool EnableExpiryTimer { get; set; }
    public bool EnableCleanupTimer { get; set; }
    public ManualResetEventSlim CleanupTimerStarted { get; }
    public Dictionary<ActiveHandlerTrackingEntry, (TaskCompletionSource<ActiveHandlerTrackingEntry>, Task)> ActiveEntryState { get; }
    internal override void StartHandlerEntryTimer(ActiveHandlerTrackingEntry entry)
    {
    if (EnableExpiryTimer)
    {
    lock (ActiveEntryState)
    {
    if (ActiveEntryState.ContainsKey(entry))
    {
    // Timer already started.
    return;
    }
    // Rather than using the actual timer on the actual entry, let's fake it with async.
    var completionSource = new TaskCompletionSource<ActiveHandlerTrackingEntry>();
    var expiryTask = completionSource.Task.ContinueWith(t =>
    {
    var e = t.Result;
    ExpiryTimer_Tick(e);
    lock (ActiveEntryState)
    {
    ActiveEntryState.Remove(e);
    }
    });
    ActiveEntryState.Add(entry, (completionSource, expiryTask));
    }
    }
    }
    internal override void StartCleanupTimer()
    {
    if (EnableCleanupTimer)
    {
    CleanupTimerStarted.Set();
    }
    }
    internal override void StopCleanupTimer()
    {
    if (EnableCleanupTimer)
    {
    Assert.True(CleanupTimerStarted.IsSet, "Cleanup timer started");
    CleanupTimerStarted.Reset();
    }
    }
    }
  • then you create ClientCount clients,
  • then you iterate over test factory's ActiveEntryState -- key is active entry, value is (completionSource, expiryTask) -- and for each kvp you (1) set completionSource's result to the key (active entry), and (2) await expiryTask
  • after that all the handlers became expired;
  • then you create ClientCount clients again (it could be the same set of names as before -- as all of the initial handlers are now expired, new should be created)
  • after that ClientCount handlers are expired, and ClientCount handlers are new and active

Copilot finished work on behalf of CarnaViire May 27, 2025 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Error loading sessions

Retrying...

Successfully merging this pull request may close these issues.

HttpClientFactory doesn't clean up after itself
6 participants