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)

Most upvoted comments

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

The opposite approach was implemented in .NET Framework, where runtime basically guaranteed the unload would happen within some reasonable time frame

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.