Skip to content
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

Scoped ServiceProvider not releasing disposing HttpClient #96296

Closed
Seabizkit opened this issue Dec 24, 2023 · 9 comments
Closed

Scoped ServiceProvider not releasing disposing HttpClient #96296

Seabizkit opened this issue Dec 24, 2023 · 9 comments
Assignees
Labels
area-Extensions-DependencyInjection needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Milestone

Comments

@Seabizkit
Copy link

Seabizkit commented Dec 24, 2023

Description

i have spent some time narrowing this down to the smallest reproducible example
consider the below...

basically when using ' using (var scope = s.ServiceProvider.CreateScope())' im expecting that anything created in this scope is disposed when the scope is ended..

this does not seem to be the case for Typed HttpClients, where you register the client with 'AddHttpClient'

my testing it looks like its only disposed of when some internal 2min for holding on the to HttpCleint has been reached.

there should be no more references to the scoped context once disposed.

public sealed class Plugin : IParadoxComponent
{
    
    private Action<IServiceCollection, IConfiguration> SetupServices()
    {
        Action<IServiceCollection, IConfiguration> @delegate = (services, config) =>
        {
            services.AddLogging();
            services.AddHttpClient<ICustomerClient, CustomClientTest>();

        };
        return @delegate;
    }


    public async Task<PluginResponse> ExecuteAsync(ExecutingContext e, CancellationToken ct = default)
    {
        var response = new PluginResponse();
        using (var s = new Startup(SetupServices()))
        {
            using (var scope = s.ServiceProvider.CreateScope())
            {
                var services = scope.ServiceProvider;

                var client = services.GetRequiredService<ICustomerClient>();

            }
        }
        return response;
    }


}

public interface ICustomerClient
{
}

public class CustomClientTest : ICustomerClient
{
    HttpClient _httpClient;
    public CustomClientTest(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

Reproduction Steps

  • create typed client
  • request typed client from within a scoped ServiceProvider
  • when the ServiceProvider is done check if HttpClient and CustomClientTest are complete free-ed
  • it will not be for 2min, which means something in AddHttpClient does not respect being created in a scope.
  • I put in loop to create many while watching memory and implenet a weekref to see when it free'ed only done after 2min

GC.Collect(); GC.WaitForPendingFinalizers();

private static void Main(string[] args)
 {
     Console.WriteLine("Hello, World!");

     for (int i = 0; i < 10000; i++) {

         var s = GetStuff();
         using (var scope = s.CreateScope())
         {
             var services = scope.ServiceProvider;
             var client = services.GetRequiredService<ICustomerClient>();
         }

         if (i % 100 == 0)
         {
             Console.WriteLine($"{i}");
             GC.Collect();
             GC.WaitForPendingFinalizers();
             Thread.Sleep(100);
         }
     }
    
 }

 public static IServiceProvider GetStuff()
 {

     ////register Configuration
     /********************************/
     var environment = Environment.GetEnvironmentVariable("ASPNET_ENVIROMENT") ??
                     Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";

     string global = Directory.GetCurrentDirectory();
     string LocalPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);


     // Create a dedicated FileProvider based on the /config directory
     var provider = new PhysicalFileProvider(LocalPath);

     var builder = new ConfigurationBuilder()
         .SetBasePath(global)
         .AddJsonFile("appsettings.json", true, false)
         .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: false)
         .AddJsonFile(provider, "appsettings.json", true, false)
         .AddJsonFile(provider, $"appsettings.{environment}.json", true, false);


     var config = builder.Build();
     /********************************/

     var services = new ServiceCollection();
     services.AddHttpClient<ICustomerClient, CustomClientTest>();

     var _serviceProvider = services.BuildServiceProvider();
     return _serviceProvider;
 }

mem should not be climbing all the time....

Expected behavior

the scoped service and all of its services should be completely disposed... including type clients

Actual behavior

scoped service hangs around until the typed client starts its internal cleanup like 2min later

Regression?

No response

Known Workarounds

dont know of any

Configuration

No response

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Dec 24, 2023
@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Dec 24, 2023
@vcsjones vcsjones added area-Extensions-DependencyInjection and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Dec 24, 2023
@ghost
Copy link

ghost commented Dec 24, 2023

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

Issue Details

Description

i have spent some time narrowing this down to the smallest reproducible example
consider the below...

basically when using ' using (var scope = s.ServiceProvider.CreateScope())' im expecting that anything created in this scope is disposed when the scope is ended..

this does not seem to be the case for Typed HttpClients, where you register the client with 'AddHttpClient'

my testing it looks like its only disposed of when some internal 2min for holding on the to HttpCleint has been reached.

there should be no more references to the scoped context once disposed.

public sealed class Plugin : IParadoxComponent
{
    
    private Action<IServiceCollection, IConfiguration> SetupServices()
    {
        Action<IServiceCollection, IConfiguration> @delegate = (services, config) =>
        {
            services.AddLogging();
            services.AddHttpClient<ICustomerClient, CustomClientTest>();

        };
        return @delegate;
    }


    public async Task<PluginResponse> ExecuteAsync(ExecutingContext e, CancellationToken ct = default)
    {
        var response = new PluginResponse();
        using (var s = new Startup(SetupServices()))
        {
            using (var scope = s.ServiceProvider.CreateScope())
            {
                var services = scope.ServiceProvider;

                var client = services.GetRequiredService<ICustomerClient>();

            }
        }
        return response;
    }


}

public interface ICustomerClient
{
}

public class CustomClientTest : ICustomerClient
{
    HttpClient _httpClient;
    public CustomClientTest(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

Reproduction Steps

  • create typed client
  • request typed client from within a scoped ServiceProvider
  • when the ServiceProvider is done check if HttpClient and CustomClientTest are complete free-ed
  • it will not be for 2min, which means something in AddHttpClient does not respect being created in a scope.
  • I put in loop to create many while watching memory and implenet a weekref to see when it free'ed only done after 2min

GC.Collect(); GC.WaitForPendingFinalizers();

private static void Main(string[] args)
 {
     Console.WriteLine("Hello, World!");

     for (int i = 0; i < 10000; i++) {

         var s = GetStuff();
         using (var scope = s.CreateScope())
         {
             var services = scope.ServiceProvider;
             var client = services.GetRequiredService<ICustomerClient>();
         }

         if (i % 100 == 0)
         {
             Console.WriteLine($"{i}");
             GC.Collect();
             GC.WaitForPendingFinalizers();
             Thread.Sleep(100);
         }
     }
    
 }

 public static IServiceProvider GetStuff()
 {

     ////register Configuration
     /********************************/
     var environment = Environment.GetEnvironmentVariable("ASPNET_ENVIROMENT") ??
                     Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";

     string global = Directory.GetCurrentDirectory();
     string LocalPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);


     // Create a dedicated FileProvider based on the /config directory
     var provider = new PhysicalFileProvider(LocalPath);

     var builder = new ConfigurationBuilder()
         .SetBasePath(global)
         .AddJsonFile("appsettings.json", true, false)
         .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: false)
         .AddJsonFile(provider, "appsettings.json", true, false)
         .AddJsonFile(provider, $"appsettings.{environment}.json", true, false);


     var config = builder.Build();
     /********************************/

     var services = new ServiceCollection();
     services.AddHttpClient<ICustomerClient, CustomClientTest>();

     var _serviceProvider = services.BuildServiceProvider();
     return _serviceProvider;
 }

mem should not be climbing all the time....

Expected behavior

the scoped service and all of its services should be completely disposed... including type clients

Actual behavior

scoped service hangs around until the typed client starts its internal cleanup like 2min later

Regression?

No response

Known Workarounds

dont know of any

Configuration

No response

Other information

No response

Author: Seabizkit
Assignees: -
Labels:

untriaged, area-Extensions-DependencyInjection

Milestone: -

@pinkfloydx33
Copy link

AddHttpClient uses IHttpClientFactory under the hood. The factory itself is registered as a singleton. However it manages the lifetime of the HttpMessageHandlers and their dependencies with their own scopes that are distinct from the scope you're creating.

Instead, HttpClientHandlerOptions.HandlerLifetime is used to determine how long a handler can be reused. As you are seeing, it defaults to 2mins

@Seabizkit
Copy link
Author

how do i allow my scope to control lifetimes

@Seabizkit
Copy link
Author

#47091

looks like its simular...

come about for me as i try to test plugins and releasing of them.. it will not and its not clear why... its the code hanging on to instance via the underlying stuff of the factory stuffs which its not disposabled even tho the scope is simplicity told its finished.. this does not respect that scope.

@steveharter steveharter self-assigned this Jan 8, 2024
@steveharter
Copy link
Member

@SeeBizkit can you clarify -- if #47091 is addressed, what work is expected to still need to be done?

Thanks

@steveharter steveharter added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jan 8, 2024
@ghost
Copy link

ghost commented Jan 8, 2024

This issue has been marked needs-author-action and may be missing some important information.

@steveharter steveharter removed the untriaged New issue has not been triaged by the area owner label Jan 8, 2024
@Seabizkit
Copy link
Author

Seabizkit commented Jan 10, 2024

@steveharter well my understanding is they are basically the same thing, [could be wrong]

my issue is there is currently no way to actually control this that im aware of or could find, [well im hoping for alternative]
yes i can set [HandlerLifetime] but this does not actually do what i want. i want its scope to be the scope of the "var scope = s.ServiceProvider.CreateScope()"

aka how would i code for this... if the answer is #47091 is addressed, then fair enough. otherwise am i missing something and this can actually be configure in such way that the scope is the scope.

if 47091 does not actually resolve this then, part of 47091 should be that this functionality behaves in such a way that one would expect.

as this is cause issue when trying to debug memory leaks and the likes..

@ghost ghost added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed needs-author-action An issue or pull request that requires more info or actions from the author. labels Jan 10, 2024
@buyaa-n buyaa-n added this to the Future milestone Jul 19, 2024
@buyaa-n
Copy link
Contributor

buyaa-n commented Jul 19, 2024

@CarnaViire is this issue can be handled with #47091? if so, we could close this as dup

@CarnaViire
Copy link
Member

Yes, I confirm this is a dup of #47091

@buyaa-n buyaa-n closed this as not planned Won't fix, can't repro, duplicate, stale Jul 19, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Aug 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Extensions-DependencyInjection needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Projects
None yet
Development

No branches or pull requests

6 participants