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?
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:
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.ContainerBuilder
inside the BeginLifetimeScope
lambda and that causes confusion and issues.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:
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.