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

ExternalRegistrySource reuses ExternalComponentRegistration instances causing ObjectDisposedException #960

Closed
sandersaares opened this issue Feb 13, 2019 · 6 comments
Labels

Comments

@sandersaares
Copy link
Contributor

sandersaares commented Feb 13, 2019

Edit: updated issue description here. Original below and in comments.

Consider a three-level lifetime scope hierarchy:

  1. root
  2. middle
  3. child

Register type T in root with InstancePerLifetimeScope.

Register some irrelevant types in middle and in child (to materialize a scope-specific registry and not reuse the copy-on-write registry).

Now resolve type T from middle. This results in a new ExternalComponentRegistration being added to the registry of middle, based on the registration in root.

Now resolve type T from child. This results in the same instance of ExternalComponentRegistration being added to the registry of child as was added to middle.

Now dispose of child. The registration still shared with middle is disposed of!

Now attempt to resolve T from middle. You get ObjectDisposedException from the activator.

The root cause is that ExternalRegistrySource.RegistrationsFor will reuse existing instances from the parent if they are already ExternalComponentRegistrations.

Proposed fix: do not reuse instances of ExternalComponentRegistration.


I am trying to debug a registration disposal issue and request hints at what to look at, so I could properly analyze the situation and formulate a sensible bug report (or discover my error).

The situation is:

Exception thrown: 'System.ObjectDisposedException' in Autofac.dll
Instances cannot be created by this activator as it has already been disposed.

I have a root scope and a child scope. Neither is disposed. I am resolving from the child scope.

The failure appears to be when resolving my type T that has a parameter of type ILifetimeScope (used by my object T to create child scopes). The registration for ILifetimeScope has IsDisposed=true (looking in childScope.ComponentRegistry.Registrations).

In fact, registrations for all types inherited via InstancePerLifetimeScope() from my root scope are also IsDisposed=true. One registration that is defined directly on the child scope is IsDisposed=false.

Example of registering a type on the root scope (registration IsDisposed=true when exception occurs)

builder.RegisterType<DefaultSynchronousRetryMechanism>()
                .As<ISynchronousRetryMechanism>()
                .InstancePerLifetimeScope();

Example of creating child scope and registering a type on it (registration IsDisposed=false):

childScope = rootScope.BeginLifetimeScope(builder =>
{
    builder.RegisterType<JobExecutionAttempt>()
        .InstancePerLifetimeScope();
});

I later create a next layer child scope and try to resolve:

using (var jobScope = childScope.BeginLifetimeScope())
{
    var attempt = jobScope.Resolve<JobExecutionAttempt>();
    await attempt.TryExecuteAsync(nextJobId.Value);
}

JobExecutionAttempt takes parameter of type ILifetimeScope which triggers ObjectDisposedException in .Resolve().

I do not expect you to have a solution for me based on this meager info but I request input on what I should be looking at. It is very much unexpected to me that ILifetimeScope and inherited registrations are IsDisposed=true. What can cause registrations to be IsDisposed=true without the lifetime scope being disposed?

This seems to happen after some time - not on the first entry into the last using block but perhaps the second?

I will continue to dig and improve the issue report once I find more relevant data.

@sandersaares
Copy link
Contributor Author

sandersaares commented Feb 14, 2019

Okay here is what I see:

  1. I have rootScope -> childScope -> jobScope [no registrations] -> actionScope layering.
  2. A using (actionScope) block ends, disposing of actionScope
  3. This results in disposal of a ScopeRestrictedRegistry with 12 registrations. Two of these are registered in actionScope configuration. The other 10 are from childScope and rootScope.

I do not understand why the other 10 registrations here are being disposed of (as they are registered in childScope/rootScope not actionScope and the same instances later used again for other actionScopes).

I write my train of thought:

  • When actionScope is built, its ScopeRestrictedRegistry only contains the 2 that were registered in actionScope configuration. The other 10 appear after first .Resolve() with Ownership=ExternallyOwned (I guess some type of import mechanism from parent scope?).
  • From this I deduce that the registrations for actionScope are specific "imports" of some sort for jobScope, even if the original registrations come from a parent scope, so it makes sense for them to be disposed. But why am I getting ObjectDisposedException later? Is something reusing the disposed activators somehow?
  • Yes, registrations on the parent scope also now have IsDisposed=true after the "imports" of the inner scope were disposed of. Why?
  • ComponentRegistry.GetInitializedServiceInfo() seems to be where the "imports" from parent scope are created. The imports come from ExternalRegistrySource.
  • Each "import" gets a new instance of DelegateActivator. Sounds good.

But no it does not? Take a look at this screenshot:

image

The registration for DefaultSynchronousRetryMechanism is imported into actionScope (first highlight). Its grandparent (childScope) also has it imported but with the same Activator instance (second highlight)! (Object ID $4). So when the actionScope is disposed, the childScope registration also has its activator disposed, even though the childScope lifetime continues.

How/why does this activator get shared between the two registrations in entirely different scopes?

The registration for this type is made on the rootScope.

@sandersaares
Copy link
Contributor Author

sandersaares commented Feb 14, 2019

Oh, I see it.

ExternalRegistrySource.RegistrationsFor has r as ExternalComponentRegistration as an alternative to creating a new registration. So if it sees that the thing it is registering is already an ExternalRegistration, it simply reuses it.

This means that my actionScope's registry reuses the ExternalComponentRegistration from its grandparent.

And then when the deeper scope is disposed, the registration instance is disposed of, even though it remains in use in the grandparent! That's not right.

@sandersaares sandersaares changed the title Unexpected IsDisposed=true on registrations - yet lifetime scope is not disposed ExternalRegistrySource reuses ExternalComponentRegistration instances, leading to use-after-dispose Feb 14, 2019
@chenmach
Copy link

chenmach commented Feb 14, 2019

I have the same problem after upgrading to 4.9.0 (4.8.X works fine) (was waiting for #218 to use contextual loggers)

I was able to reproduce it using this sample :
https://gist.github.com/chenmach/fe9c029d84f2583dee6837741d5b2296

Edit
@sandersaares suggestion - to remove the r as ExternalComponentRegistration and create newly fresh one every time solve it and all other tests are passed

chenmach added a commit to wearegofer/Autofac that referenced this issue Feb 14, 2019
@sandersaares
Copy link
Contributor Author

Looks like #946 (included in 4.9.0) made it visible by throwing the exception, although the use-after-dispose was already happening (silently) in earlier versions.

@alexmg
Copy link
Member

alexmg commented Feb 18, 2019

Thank you @sandersaares and @chenmach for the help. Looks like something that crept in under the radar a while ago.

I added a few more assertions to the unit test from @sandersaares and confirmed the issue fixes the scenario in the Gist from @chenmach.

@alexmg alexmg changed the title ExternalRegistrySource reuses ExternalComponentRegistration instances, leading to use-after-dispose ExternalRegistrySource reuses ExternalComponentRegistration instances causing ObjectDisposedException Feb 18, 2019
@alexmg
Copy link
Member

alexmg commented Feb 18, 2019

I have released 4.9.1 with this fix to NuGet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants