runtime: AssemblyLoadContext.Unload silently fails to unload Assemblies, leaking filehandles
Issue Title
The Unload
method does not free any assemblies returned from LoadFromStream
that are still referenced somewhere.
General
netcoreapp3.1
public class TestAssemblyLoader : AssemblyLoadContext, IAssemblyLoader
{
public TestAssemblyLoader() : base(true)
{
LoadedAssemblies = new Dictionary<string, Assembly>();
Resolving += LoadContext_Resolving;
}
private Assembly LoadContext_Resolving(AssemblyLoadContext arg1, AssemblyName arg2)
{
return LoadedAssemblies[arg2.FullName];
}
private Dictionary<string, Assembly> LoadedAssemblies { get; }
Assembly IAssemblyLoader.LoadFromStream(Stream stream)
{
var assembly = LoadFromStream(stream);
return LoadedAssemblies[assembly.FullName] = assembly;
}
protected sealed override Assembly Load(AssemblyName assemblyName)
{
return null;
}
public void Dispose()
{
LoadedAssemblies.Clear(); // Without this call, the assemblies in this dictionary don't get unloaded.
Unload();
}
}
Symptom is visible in unreleased file handles - example repro here: https://github.com/dave-yotta/roslyn-assemblyunload
It might make sense for this to be expected behaviour, but I certainly didn’t expect it. (nor did our production machines 😢)
At least can there be an argument to unload like bool throwIfAnyUnloadFailed = false
so that it’s self-documenting to callers somehow?
Related issue I’m coming from here: https://github.com/dotnet/roslyn/issues/49282
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 23 (13 by maintainers)
Please note that there is nothing that we can qualify as unload failure. There is no deadline after which we would consider the unloading as failed in the runtime. So if I take it to the extreme, you can call Unload and hold a reference to something inside of the context. If you release that reference a week later, the unload will complete at that point. The initiated / completed event should not be problematic to add. Regarding the “not yet unloadable” - there is no periodic checking happening. It is all GC driven. So when the last reference to an assembly is gone and GC collects it, it results in removing a reference to the LoaderAllocator managed instance (that reference is stored in the Assembly’s SyncRoot). So there is no explicit walking of a list of assemblies in the AssemblyLoadContext and checking whether they can be unloaded. Btw, if you are interested in the low level behavior, there is an image of the dependency references that keep AssemblyLoadContext here: https://github.com/dotnet/runtime/blob/master/docs/design/features/unloadability.md#assemblyloadcontext-unloading-process
FWIW, AppDomain unloading was not guaranteed to succeed. It could timeout out and fail too, e.g. when the AppDomain was stuck in unmanaged code.