Search code examples
c#memory-leaksautofac

Using RegisterInstance in BeginLifetimeScope configuration causes memory leak


I have the following usage of Autofac

public class Executor 
{
  private readonly ILifetimeScope rootScope;

  public Executor(ILifetimeScope rootScope)
  {
     this.rootScope = rootScope;
  }

  public async Task ExecuteAsync(IData data)
  {
    using (ILifetimeScope childScope = this.rootScope.BeginLifetimeScope(builder =>
    {
      var module = new ExecutionModule(data);
      builder.RegisterModule(module);
    }))
    {
        // resolve services that depend on IData and do the work
    }
  }
}

public class ExecutionModule : Module
{
  private readonly IData data;

  public ExecutionModule(IData data)
  {
    this.data = data;
  }

  protected override void Load(ContainerBuilder builder)
  {
    builder.RegisterInstance(this.data).As<IData>();
  }
}

ExecuteAsync is called a lot of times during application runtime and IData represents input from the outside (varies for each ExecuteAsync) that should be shared with the services inside of the lifetime scope.

After running the app for a while I started experiencing memory exhaustion. Later via additional profiling I determined that instances of IData survived garbage collections and seem to be causing a memory leak.

As an experiment, the registration of ExecutionModule (i.e. RegisterInstance also) was removed from the code, and the memory leak was eliminated.

Autofac's code has interesting remarks for BeginLifetimeScope:

/// The components registered in the sub-scope will be treated as though they were
/// registered in the root scope, i.e., SingleInstance() components will live as long
/// as the root scope.

The question is: if RegisterInstance means registering singleton then in this case instances of IData will live as long as the root scope even they can only be resolved inside a childScope where it was registered, am I right?


Solution

  • I think that comment probably needs to be revisited and fixed up. A lot has changed in the (looks like six?) years since that was added to the ILifetimeScope interface and while the internals changed, the interface didn't so the docs haven't been revisited. I've added an issue to get those docs updated.

    Here's a quick test I ran using Autofac 6:

    using System;
    using Autofac;
    using Autofac.Diagnostics;
    
    namespace AutofacDemo
    {
        public static class Program
        {
            public static void Main()
            {
                var builder = new ContainerBuilder();
                builder.RegisterType<Dependency>().SingleInstance();
                using var container = builder.Build();
    
                var dep = container.Resolve<Dependency>();
                Console.WriteLine("Dependency registered as single instance in root scope:");
                Console.WriteLine("ID: {0}", dep.Id);
                Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
                Console.WriteLine();
    
                var rootScopeTag = dep.ScopeTag;
    
                using var standaloneScope = container.BeginLifetimeScope();
                dep = standaloneScope.Resolve<Dependency>();
                Console.WriteLine("Dependency registered in root, resolved in child scope:");
                Console.WriteLine("ID: {0}", dep.Id);
                Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
                Console.WriteLine("Resolved from root? {0}", dep.ScopeTag == rootScopeTag);
                Console.WriteLine();
    
                using var singleInstanceScope = container.BeginLifetimeScope(
                    b => b.RegisterType<Dependency>()
                        .SingleInstance());
                dep = singleInstanceScope.Resolve<Dependency>();
                Console.WriteLine("Dependency registered as single instance in child scope:");
                Console.WriteLine("ID: {0}", dep.Id);
                Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
                Console.WriteLine("Resolved from root? {0}", dep.ScopeTag == rootScopeTag);
                Console.WriteLine();
    
                var instance = new Dependency();
                using var registerInstanceScope = container.BeginLifetimeScope(
                    b => b.RegisterInstance(instance)
                        .OnActivating(e => e.Instance.ScopeTag = e.Context.Resolve<ILifetimeScope>().Tag));
                dep = registerInstanceScope.Resolve<Dependency>();
                Console.WriteLine("Dependency registered as instance in child scope:");
                Console.WriteLine("ID: {0}", dep.Id);
                Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
                Console.WriteLine("Resolved from root? {0}", dep.ScopeTag == rootScopeTag);
            }
        }
    
        public class Dependency
        {
            public Dependency()
            {
                this.Id = Guid.NewGuid();
            }
    
            public Dependency(ILifetimeScope scope)
                : this()
            {
                this.ScopeTag = scope.Tag;
            }
    
            public Guid Id { get; }
    
            public object ScopeTag { get; set; }
        }
    }
    

    What this does is try out a few ways to resolve a .SingleInstance() or .RegisterInstance<T>() component and show which scope they actually come from. It does this by injecting ILifetimeScope into the constructor - you'll always get the lifetime scope from which the component is being resolved. The console output for this looks like:

    Dependency registered as single instance in root scope:
    ID: 056e1584-fa2a-4657-b58c-ccc8bfc504d2
    Scope tag: root
    
    Dependency registered in root, resolved in child scope:
    ID: 056e1584-fa2a-4657-b58c-ccc8bfc504d2
    Scope tag: root
    Resolved from root? True
    
    Dependency registered as single instance in child scope:
    ID: 3b410502-b6f2-4670-a182-6b3eab3d5807
    Scope tag: System.Object
    Resolved from root? False
    
    Dependency registered as instance in child scope:
    ID: 20028683-23c1-48a0-adbe-94c3a8180ed7
    Scope tag: System.Object
    Resolved from root? False
    

    .SingleInstance() lifetime scope items will always be resolved from the lifetime scope in which they're registered. This stops you from creating a weird captive dependency problem. You'll notice this in the first two resolution operations - resolving both from the root container and a nested scope shows the scope tag is the root scope tag, so we know it's coming from the container in both cases.

    RegisterInstance<T> doesn't have dependencies because it's constructed, but I simulate the functionality by adding an OnActivating handler. In this case, we see the object actually gets resolved from a child lifetime scope (sort of an empty System.Object tag) and not the root.

    What you'll see in the Autofac source is that any time you BeginLifetimeScope() it creates a "scope restricted registry" which is where the components registered in the child scope are stored. That gets disposed when the scope is disposed.

    While I can't guarantee what you're seeing isn't an issue, my experience has been that the simplification of code into a StackOverflow question can hide some of the complexities of a real world system that are actually important. For example:

    • If the IData is disposable, it'll get held until the child lifetime scope is disposed; and if the disposal of the child scope isn't getting called, that's a leak.
    • I've seen folks who accidentally use the original ContainerBuilder inside the BeginLifetimeScope lambda and that causes confusion and issues.
    • If there's something more complex about the threading or execution model that could stop disposal or abort the async thread before disposal occurs, that could be trouble.

    I'm not saying you have any of these problems. I am saying that given the above code there's not enough to actually reproduce the issue, so it's possible the leak is a symptom of something larger that has been omitted.

    If it was me, I'd:

    • Make sure I was on the latest Autofac if I wasn't already.
    • See if it makes a difference registering the data object not inside a module. It shouldn't matter, but... worth a check.
    • Really dive into the profiler results to see what is holding the data objects. Is it the root container? Or is it each child scope? Is there something holding onto those child scopes and not letting them get disposed or cleaned up?

    And, possibly related, we're about to cut a release that includes a couple of fixes for ConcurrentBag usage on some platforms which might help. That should be 6.2.0 and I'll hopefully get that out today.