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

Replace reflection only load context with shared metadataLoadContext for publishing packages. Solution 1. #14576

Merged
merged 21 commits into from
Nov 9, 2023

Conversation

mjkkirschner
Copy link
Member

@mjkkirschner mjkkirschner commented Nov 6, 2023

Purpose

ReflectionOnlyLoadContext is no longer supported in net6. We use it when publishing packages to allow authors to add and remove dependencies of different versions of assemblies since those loads would usually fail if loaded in the same context as the running application.

A lot of this can be improved later when/if we move to assembly load contexts per package - but we'll need to do that when we have more time.

For now this tries to maintain similar behavior to the legacy behavior - it's not exact as MLC do not behave exactly like ReflectionOnlyLoadContexts -

This PR uses one shared static MLC, analogous to the shared reflection only load context that lived for application lifetime.

This PR also adds a new dependency on version 6 of the meta data load context package.

Questions for the team:

  • do I need to add something to the about box for the metaDataLoad package? I think it's also in the runtime folder. (ie part of dotnet runtime)

Declarations

Check these if you believe they are true

  • The codebase is in a better state after this PR
  • Is documented according to the standards
  • The level of testing this PR includes is appropriate
  • User facing strings, if any, are extracted into *.resx files
  • All tests pass using the self-service CI.
  • Snapshot of UI changes, if any.
  • Changes to the API follow Semantic Versioning and are documented in the API Changes document.
  • This PR modifies some build requirements and the readme is updated
  • This PR contains no files larger than 50 MB

Release Notes

Move from ReflectionOnlyLoadContext to MetadataLoadContext for publishing packages.

Copy link

github-actions bot commented Nov 6, 2023

⚠️ [run-bin-diff-net60-windows] - Files Added/Deleted::1 new file(s) have been added
(Updated: 2023-11-09-20:24:08)

@QilongTang QilongTang added this to the 3.0 milestone Nov 7, 2023
var resolver = new PathAssemblyResolver(assemblyPaths);
var mlc = new MetadataLoadContext(resolver);

//TODO for now, we leak the mlc, but it's unlikely users will publish
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are we leaking the mlc ?
is that why we are not cleaning up the mlc ?
There might be a lot of assemblies loaded from the runtime libs....are you saying it is not probable that a user publishes multiple times in the same session ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I am not disposing the mlc here - its assemblies seem to live as package dependencies for quite some time. If we moved to only using one MLC per session I think there would be a smaller chance of any appreciable memory leak - ie it would be more like using the ReflectionLoadContext.

Yes I think it's unlikely that a user publishes multiple times in a single session.

At this point it's not clear to me that there is ever a good time to dispose these mlcs.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when you dispose the mlc at the end of this function? Is it holding onto assembly references that are also owned by the package?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assemblies loaded in the mlc seem to be stored in the package view model
As Mike said, it is not obvious how long they are kept around

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we try to use a single mlc, would that increase the chance of conflicts that were not there with reflectionOnly ?
Also, seems like mlc is a little more permissive than reflectionOnly. As an example I think BadImageFormatException exceptions will not eever be thrown.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test this feature as much as possible (in revit and sandbox) since we do not know all the implications yet

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pinzart90 I've tested your concern, and even though the docs don't mention it, it throws the same exception when loading a native assembly.
Screenshot 2023-11-08 at 9 19 13 PM

Also I have written a test for it.

//where reflection only context would throw FileLoadException
//we try to maintain that legacy behavior for Dynamo 3.0.
if (mlc.GetAssemblies().Select(x => x.GetName() == assemName).Any()) {
throw new FileLoadException(filename);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this happen ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it happens in the tests that I have removed the failure category from in this PR. They rely on these exceptions being thrown for .dlls with the same mvid.

the metaDataLoadContext will throw an exception by default for assemblies with the same name with different mvids - but apparently the behavior for reflection only load context was different.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made this smarter I think.

@mjkkirschner mjkkirschner changed the title Replace reflection only load context with metadataLoadContext for publishing packages. Replace reflection only load context with shared metadataLoadContext for publishing packages. Solution 1. Nov 9, 2023
@@ -17,7 +18,7 @@ namespace DynamoCoreWpfTests
public class PublishPackageViewModelTests: DynamoViewModelUnitTest
{

[Test, Category("Failure")]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be passing... this test is unrelated to this task so if it fails on CI I will just revert this.

var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll");
// Create PathAssemblyResolver that can resolve assemblies using the created list.
var resolver = new PathAssemblyResolver(runtimeAssemblies);
return new MetadataLoadContext(resolver);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we no longer doing this:

//and all the package assemblies.
            foreach (var assemblyFile in pkg.EnumerateAssemblyFilesInPackage())
            {
                assemblyPaths.Add(assemblyFile);
            }

            // Create PathAssemblyResolver that can resolve assemblies using the created list.
            var resolver = new PathAssemblyResolver(assemblyPaths);
            var mlc = new MetadataLoadContext(resolver);

Copy link
Member Author

@mjkkirschner mjkkirschner Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that it does not appear to be necessary to add the assemblies to the MLC as we pass the full path to the file we want to load.

Additionally, we only need the dependencies loaded if want to inspect those types - but we don't do much with the assemblies as far as I can tell except for accessing the location and assembly name.

In an MLC the dependencies are not required to load the assembly AFAIK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see any immediate need to use those types...but as soon as we do ...we will have exceptions thrown. THese assemblies are not exposed publicly right ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, and in addition this would have caused the same issue with ReflectionOnlyLoadContext.

Copy link
Member Author

@mjkkirschner mjkkirschner Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, the PublicPackageViewModel.Assemblies property is public and so is the class. These classes are only used in the UI layer though AFAIK and are not easily accessible from anywhere but our views, ie not from node or extensions without clients hacking around.

Copy link
Contributor

@aparajit-pratap aparajit-pratap Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I'm a bit confused after reading this, just trying to understand it better. The docs seem to say that the resolver that is passed to the mlc resolves assemblies that are dependencies of the assembly being loaded (by the mlc). I can understand that this should include runtime assemblies, which most likely the assembly being loaded (for inspection) has a dependency on but won't it also have dependencies most likely on the other helper assemblies that are in the same package folder itself for the package that's being published? Or are you saying that since the assembly being loaded and the other assemblies are in the same location, that isn't required? Maybe I'm missing something.

Copy link
Member Author

@mjkkirschner mjkkirschner Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the binary we are loading very likely will have dependencies on other binaries in the package - but it doesn't matter. It does not matter because the MLC won't fail to load a binary simply because a dependency is not loaded.

It only becomes an issue when you try to inspect a type and that type has a dependency on an assembly the resolver cannot resolve.

But in our case, as far as I can tell, we don't do any inspection, we just look at the assembly name and location... we're just using the MLC to do some sanity checking that the assembly is a real managed assembly.

ReflectionLoadContext also behaved like this, except instead of a pathresolver, you had to implement the ReflectionOnlyAssemblyResolve which we did not do.

//if loading the assembly did not actually add a new assembly to the MLC
//then we've loaded it already, and our current behavior is to
//disable publish when a package contains the same assembly twice.
if (mlcAssemblies2.Count() == mlcAssemblies.Count())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the best way to check if the assembly has been loaded successfully ?
assem = mlc.LoadFromAssemblyPath(filename);
assem != null is not good enough ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not checking if the assembly has been loaded successfully, I am checking if it had been loaded already.

Copy link
Contributor

@pinzart90 pinzart90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple of questions then LGTM

@mjkkirschner
Copy link
Member Author

@mjkkirschner mjkkirschner merged commit ace131f into DynamoDS:master Nov 9, 2023
17 checks passed
@mjkkirschner mjkkirschner deleted the pkgtest branch November 9, 2023 23:58
@@ -99,7 +100,34 @@ public void CanPublishLateInitializedJsonCustomNode()
}


[Test, Category("Failure")]
[Test]
public void NewPackageDoesNotThrow_NativeBinaryIsAddedAsAdditionalFile_NotBinary()
Copy link
Contributor

@aparajit-pratap aparajit-pratap Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the dependencies field in the pkg.json being empty indicate that the native assembly is not added as a dependent binary but as an additional file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the dependencies field points to other packages

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

Successfully merging this pull request may close these issues.

4 participants