Skip to content

Memory Leak Due to ThreadLocalStorage / ConcurrentBag / LifetimeScope Behavior in Owin #25

@ivanpfeff

Description

@ivanpfeff

Bug Description

I would love to give a concise description of exactly what this bug is, but I don't fully understand it myself so I will instead try to explain as much of what I know about the issue and hope someone can fill in the gaps.

A web application using the Owin integration has memory leaks if it is under consistent stress which does not allow the request processing threads to exit.

LifetimeScopes are created for each request and are disposed properly when the request ends, but the LifetimeScope references are maintained and as a result garbage collection never cleans up the LifetimeScopes.

Here's a sample reference tree for one such LifetimeScope:
image

Rough outline of what happens to cause this issue:

  • Autofac.Owin creates a LifetimeScope in the Owin pipeline and registers the OwinContext as an IOwinContext in this LifetimeScope. This OwinContext has an Environment key which tracks the LifetimeScope (something like "autofac:OwinLifetimeScope:")
  • The ComponentRegistration for this OwinContext is somehow tracked in a ConcurrentBag, which apparently uses ThreadLocal as some kind of backing store
  • The ThreadLocals have a strong GC handle tracking them, so they do not get cleaned up by the GC unless the thread exits. This does not happen if the web application is consistently busy.
  • Since the ThreadLocals don't get GC'ed, neither does the ComponentRegistration, the ComponentRegistrations references the OwinContext, the OwinContext references the LifetimeScope, and the LifetimeScope never gets garbage collected.

I believe the ConcurrentBag which tracks the ComponentRegistrations is coming from somewhere within the Autofac 6.0 pipeline but I am not sure where.

Steps to Reproduce

I have attached a sample application (see below), this is just a simple Web API .NET Framework application to which I added the Owin pipeline and Autofac Owin integration.

To reproduce, just spam the application (using F5 in a browser works for me) and then take a memory dump. Don't wait too long between spamming and taking the dump or the threads will exit and the GC will clean up the ThreadLocal stuff and eventually clear out the LifetimeScopes. Inside of the memory dump, search for all instances of LifetimeScope and see that there are many hanging around and are all rooted in ConcurrentBags / ThreadLocals.

If you consistently ping the sample site for an extended period of time, you will see the number of LifetimeScopes in the memory dump continue to grow.

Expected Behavior

Under high load, the LifetimeScopes which are created by the Autofac.Owin integration should be cleaned up and released for garbage collection when the request ends.

Dependency Versions

Autofac 6.0
Autofac.Owin 6.0

Additional Info

I was able to work around this problem by specifically removing the OwinContext.Environment key "autofac:OwinLifetimeScope:" which removes the reference to the LifetimeScope and allows it to be garbage collected. This smells like a hack to me.
This can be found in NonAutofacMiddleware.cs inside of the sample application.

Sample Application

MemoryLeakSample.zip

If you want to see the results in memory from running requests for an extended amount of time, I wrote a small console app to just repeatedly ping the site:

static void Main(string[] args)
{
    var address = "https://localhost:44387/";
    while (true)
    {
        Console.WriteLine($"Pinging {address} at {DateTime.Now}");
        var client = new HttpClient();
        var response = client.GetAsync(address).ConfigureAwait(false).GetAwaiter().GetResult();
        response.EnsureSuccessStatusCode();

        Thread.Sleep(300);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions