-
-
Notifications
You must be signed in to change notification settings - Fork 835
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
Isolate service information computed in child scopes from the parent scope #1350
Isolate service information computed in child scopes from the parent scope #1350
Conversation
…n of enumerables at a lower level.
…ct of inserting the collection builder in each scope. Optimise the adding of sources to the nested scope's registry by adding them directly to the tracker rather than relying on the `ContainerBuilder` for it. Revert "Optimise the adding of sources to the nested scope's registry by adding them directly to the tracker rather than relying on the `ContainerBuilder` for it." This reverts commit f6ee0faf86cd8dd55cd12b5af30d92669048b792.
…ng them directly to the tracker rather than relying on the `ContainerBuilder` for it.
Codecov ReportBase: 78.12% // Head: 78.53% // Increases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## develop #1350 +/- ##
===========================================
+ Coverage 78.12% 78.53% +0.40%
===========================================
Files 197 199 +2
Lines 5636 5715 +79
Branches 1149 1161 +12
===========================================
+ Hits 4403 4488 +85
+ Misses 717 715 -2
+ Partials 516 512 -4
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
On first glance I don't think this looks too complicated. I do worry a little about the allocations in wrapping those services in a special decorator during query, but maybe I shouldn't worry so much about it. What other registration sources would potentially need the Re How does ACTNARS get affected here? That thing has always been a thorn in my side. I can't say I 100% grok the whole of it yet; I'm swamped on some other day-job stuff so my mind is in PowerShell-module-land rather than nuances-of-Autofac-land. It'll take me a couple more read-throughs to really get it. |
I think my example on perf-impact concern was a bit sub-par; of course, if the type doesn't exist anywhere it'll throw, so not an issue. However, if a service is defined in the child scope, but in no parent, each parent will get asked about the service in each new child scope, and will not have a cached "no registrations" service info. E.g. var builder = new ContainerBuilder();
var container = builder.Build();
using (var outerScope = _container.BeginLifetimeScope(builder => {}))
{
var scopeCount = 0
while (scopeCount < 100)
{
using (var requestScope = outerScope.BeginLifetimeScope(builder => builder.RegisterType<MyService>()))
{
requestScope.Resolve<MyService>();
}
scopeCount++;
}
} In the above example, each time In the current code, the In the new code, the Thus, each time I'm a little concerned that's going to be too high a price to not have this isolation gated behind an opt-in. I also think the concern over allocating an additional I'm continuing to mull options for this that don't require opt-in. |
That makes sense... and it all sounds expensive. 💸 Without looking at the code personally... what if the dependency for caching was inverted? The child scope queries the parent to see if it can fulfill the service, but instead of the parent caching that info, the child does - then it knows to bypass that query entirely on the next request. Again, I have totally not looked at the code and I'm on my phone right now, but it seems like something should cache it, and if it's not the parent...🤷♂️ |
Well, the caching by the child scope is basically what happens already. Only the parent scopes see Problem is, that child scope could last for just the lifetime of a request and then be discarded, losing that cached info. |
Okay, I'm with you now. Hmm. |
One nice thing about using an opt-in approach using (for example) For example: var builder = new ContainerBuilder();
var container = builder.Build();
using (var isolatedScope = _container.BeginIsolatedLifetimeScope(builder =>
{
// Load some plugin assemblies, whatever is required.
}))
{
using (var requestScope = outerScope.BeginLifetimeScope(builder => builder.RegisterType<MyService>()))
{
requestScope.Resolve<MyService>();
}
} In this model, external registry queries from As long as the |
Seems reasonable. I anticipate the potential desire to replace the root lifetime for a web app coming from this next. However, I feel like that'd potentially leave the app in an inconsistent state... ... Which is the whole reason we didn't keep mutable containers in the first place, so I'd be inclined to deny that request, should it arise. |
I spent a bunch of time this morning thinking about the name of an So, I think the best option is to actually associate the method to its purpose, that is, to support the use of
By requiring the user to provide an The addition of methods it |
Good call on tying the load/unload style of lifetime scope to the assembly load context. It definitely makes it clearer, and I agree on the improvement in naming. I also like the check for |
…ion of large comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple questions/comments in there, mostly around documentation and helping folks use the feature.
test/Autofac.Test.Scenarios.LoadContext/Autofac.Test.Scenarios.LoadContext.csproj
Outdated
Show resolved
Hide resolved
src/Autofac/Features/Collections/CollectionRegistrationSource.cs
Outdated
Show resolved
Hide resolved
src/Autofac/Core/Registration/ExternalRegistryServiceMiddlewareSource.cs
Outdated
Show resolved
Hide resolved
/// </code> | ||
/// </example> | ||
/// <remarks> | ||
/// When the returned lifetime scope is disposed, the provided |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like there's a gotcha around HasCustomServiceMiddleware
such that if it has custom middleware it may not be able to be unloaded (see DefaultRegisteredSerivcesTracker
). It may be worth mentioning that here, or possibly in documentation around the feature. That won't be immediately obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think custom middleware from a loaded assembly context should work just fine...in that once the scoped registry is disposed, the custom middleware reference should be lost and the context becomes unloadable. I'll add a test for that too.
src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs
Outdated
Show resolved
Hide resolved
….LoadContext.csproj Co-authored-by: Travis Illig <tillig@paraesthesia.com>
…/github.com/autofac/Autofac into feature/do-not-retain-isolated-service-info
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good. I think there are just a couple of tests remaining.
src/Autofac/Features/Collections/CollectionRegistrationSource.cs
Outdated
Show resolved
Hide resolved
Enables build warnings when Roslyn style analyzers are hit so we don't end up with issues only in CI.
…lies referenced by a type. Also added tests for ACTNAR and Collection sources (which prompted the above change).
Right, sorry this has taken longer than I expected, but I ran into a bit of a problem when running the Basically, when disposing of a "load context aware" lifetime scope, and checking the assembly of each cache entry key to determine whether or not the entry needs to be removed from the cache, it turned out (seems obvious with hindsight), that Seeking a generic solution to this problem, I implemented This works just fine (have added some separate tests for |
Had to amend the build to ensure we build our LoadContext scenario project first before running tests, because we intentionally don't reference the project directly, so it won't be built automatically. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking better. A couple of comments about duplicate code or whatever, but I think this is super close.
I just thought of another test, and I don't know if it indicates I'm not up on how type references are held in an assembly load context, but... transitive references.
So let's say you have two assemblies.
- Assembly 1 has the type
A.Service1
in it. - Assembly 2 has the type
B.Service2
in it. A.Service1
has a propertypublic B.Service2 MyService {get; private set;}
but that property is not registered with Autofac.A.Service1
has a methodpublic void SetService(B.Serivce2 service)
that sets the property. No method injection is set up for that with Autofac.
Is there any way that we'd end up somehow with a reference to B.Service2
somewhere that would stop the context from being unloaded? I don't think so as I basically think this through out loud, but there's just something in the back of my mind itching at me about "what happens with transitive references that maybe we don't interact with?"
Actually, writing this out, if someone has used method injection, are we holding onto any references to those event handlers by the time the assembly load context is going to be declared safe for unload? I... don't think so, but figured I'd raise it.
if (kvp.Key is Type keyType) | ||
{ | ||
if (predicate(keyType.Assembly, keyType)) | ||
TypeAssemblyReferenceProvider.PopulateAllReferencedAssemblies(keyType, reusableAssemblySet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious if this if/elseif thing can be consolidated somehow since it's identical code in the blocks. I mean, I don't care a lot but it seems duplicate-y.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also looks like this exact if/else shows up in ReflectionCacheTupleDictionary
under GetKeyAssemblies
. Unclear if there's value in some sort of GetKeyAssemblies
internal sort of method that handles the Clear()
and PopulateAllReferencedAssemblies
manipulation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aye, there is some duplication there; I've factored the if is Type
check out into a new TypeAssemblyReferenceProvider.PopulateAllReferencedAssemblies
method that accepts a MemberInfo
.
I've included the "reset" of the pre-allocated hashset in the factored-out method as well, although I will say that while I can't quite put my finger on it, it feels icky somehow to reset a locally-defined set inside another method external to the class; introduces coupling of behaviour maybe? Perhaps I'd already done that by accepting a pre-allocated set. Anyway, see what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine. I thought about signaling the modifiability by passing it as ref
or something, but it's all internal and the kinda icky "side effect" of clearing the set should probably just be known given the context.
Assuming we have a custom ALC So, when we discard the assembly context, if there is any references to I am however going to add a test to verify that if a module in a loaded assembly registers an OnActivated callback and a LifetimeScopeEnding callback where those callbacks are defined in the loaded assembly, that we can still unload. I don't foresee there being an issue though. I'll also note that this is obviously best-effort; if someone captures a reference to something in a loaded assembly that we aren't tracking, unload likely won't work. |
OK, added those extra tests for method injection, no issues. Fundamentally those method injections are actually added as custom middleware to the registration, so they will all get discarded when the component registry goes away. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⛵
Looks great. I didn't merge it in case you had other stuff you wanted to throw in, but I think it's fine as-is. |
Thanks, merged. I will need to add docs for this when we release. Is there anything else planned that would go into the v7 release? Or is this the last item? |
I think we caught all the major ones - marked that delegate factory thing obsolete, added this, added the required property support... I haven't searched the issues yet for any mention of "major" but I think this was it. |
This fixes #1324, and allows an assembly context loaded inside a lifetime scope to be unloaded after the lifetime scope is disposed, without disposing of the container. I've tested that this is actually true with an assembly-unload-test-harness of sorts.
These changes still require the shared
ReflectionCacheSet
to be cleared at the end of the scope, but I've got a test method for creating a lifetime scope for the context that handles this fairly readily:Note that I have not added that method in my current changes, although there is an argument for it, more on that in a moment.
Raising this as draft for two reasons; first of all, I need to add some tests, and secondly, there are 'some' performance implications for this change that mean it may need consideration/tweaking before it goes out.
What have I done?
When the
ExternalRegistrySource
asks a parent registry "what are the registrations for this service", it wraps theService
in aScopeIsolatedService
instance before sending it to the parent registry.When evaluating the service information for a service wrapped in
ScopeIsolatedService
, if the process of evaluating the service results in no registrations for that service, theDefaultRegisteredServicesTracker
does not keep the reference to the service information, discarding it from the dictionary of all service info at the end of the initialisation process.This does cause a slight annoyance with
CollectionRegistrationSource
, where that source always returns a registration for the type, even if actually evaluating it would return an empty set.To address that, there is a new interface
IPerScopeRegistrationSource
, applied toCollectionRegistrationSource
, that indicates it is added as a source to all new scopes, so that the creation of that registration happens in the local scope; sources marked withIPerScopeRegistrationSource
are also never invoked when aScopeIsolatedService
is being evaluated in the parent services tracker.To try and address the performance overheads of not having the single
CollectionRegistrationSource
, I have pushed the generated factory methods into the reflection cache.Perf Impact / Opt-in?
This change will have some performance impact if someone is frequently creating a new scope with it's own registry, and requesting a service that doesn't exist in any parent registry, e.g.
This is because each scope above the
requestScope
would basically have to do service initialisation again each time it's resolved. Not ideal, certainly.So...the question then is whether that's ok (for simplicity reasons), or we make people opt in to this behaviour where we wrap a service in
ScopeIsolatedService
, e.g. the example I have above,BeginLifetimeScopeForAssemblyLoadContext
.