Skip to content

Commit ce441e0

Browse files
daxian-dbwjoeyaiello
authored andcommitted
Merge RFC0043 on supporting module isolation as withdrawn (#164)
* module-isolation draft * RFC for Loading-Module-Into-Isolated-AssemblyLoadContext * Update the comment due date * Minor update * Updates about 'AssemblyResolve' event * Merge RFC0043 on module isolation as withdrawn
1 parent 98c6118 commit ce441e0

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
---
2+
RFC: RFC0043
3+
Author: Dongbo Wang
4+
Status: Withdrawn
5+
SupercededBy: N/A
6+
Version: 1.0
7+
Area: Microsoft.PowerShell.Core
8+
Comments Due: 4/10/2019
9+
Plan to implement: No
10+
---
11+
12+
# Loading Modules into Isolated AssemblyLoadContext
13+
14+
Assembly isolation is a missing feature in PowerShell.
15+
Today, a PowerShell process loads all assemblies in the default load context.
16+
When an assembly is already loaded, it's not allowed to load the same assembly again with a different version.
17+
This results in the PowerShell issue [#2083](https://github.com/PowerShell/PowerShell/issues/2083).
18+
Basically, when two modules depend on different versions of the same assembly,
19+
if the module that depends on the lower version is loaded first,
20+
the other module cannot be loaded in the same session anymore.
21+
22+
There are two layers of assembly isolation that we can consider to support
23+
24+
- Runspace level: Load PowerShell assemblies into a custom `AssemblyLoadContext`,
25+
then create a Runspace via Reflection within that `AssemblyLoadContext`.
26+
So that Runspace works with its own set of PowerShell assemblies in that load context,
27+
and all assemblies loaded in that Runspace also get loaded into that load context
28+
(Except for `Assembly.LoadFrom` and `Assembly.LoadFile`.
29+
`LoadFrom` always loads an assembly to the default load context;
30+
`LoadFile` always loads an assembly into a new load context).
31+
32+
- Module level: Load a module into a custom `AssemblyLoadContext`,
33+
the module required assemblies as well as all the assemblies loaded by `Add-Type` in the module script will be loaded into that load context.
34+
35+
Note: _This RFC focuses on the module-level isolation, **but it's written with the purpose to be withdrawn**._
36+
_While researching the feasibility of this idea, I realized that it's impossible to have it well supported due to the challenges discussed below._
37+
_This RFC serves as a summary of the research to explain the reasons why we will not proceed with this idea._
38+
39+
## Motivation
40+
41+
As a PowerShell user, I'm able to use modules that depend on different versions of the same assembly in the same session.
42+
43+
## Specification
44+
45+
### Import-Module -Isolated
46+
47+
The idea is simple: Add the switch parameter `-Isolated` to `Import-Module`.
48+
When specified, PowerShell creates a custom `AssemblyLoadContext` (ALC) instance and makes sure all assembly loadings for and within that module,
49+
except for explicit user calls to `Assembly.LoadFrom` and `Assembly.LoadFile`,
50+
load the assemblies into that custom load context.
51+
The `ModuleInfo` object will hold a reference to the load context used for this module.
52+
53+
PowerShell assembly loading code and `Add-Type` needs to be changed to not call `Assembly.LoadFrom`.
54+
Instead, PowerShell needs to call `LoadFromAssemblyPath` on the corresponding load context instance:
55+
56+
- It queries for the current `EngineSessionState` to see if it's from an isolated module;
57+
- If yes, then the load context of that module is used, otherwise, default load context `AssemblyLoadContext.Default` is used.
58+
59+
_This means we will have type identity issues within a session_ -- type C and C' are the same type in terms of the fully-qualified type name,
60+
but they are from two different assembly instances that are loaded into two load contexts.
61+
This would raise multiple folds of challenges that will be discussed below
62+
63+
### Interoperability
64+
65+
Imagine the assemblies A and B are loaded into ALC-M (isolated module M).
66+
Assembly A exposes a cmdlet `Demo-Cmdlet` that has a parameter accepting an object of type B::C.
67+
Let's say a different version of B is loaded into the default-ACL, and the result assembly instance is B'.
68+
So, from the default-ACL (default Runspace), one can create an object of B'::C' and call `Demo-Cmdlet` with it.
69+
That call will result in a type casting exception with the confusing error like "cannot cast B::C to B::C".
70+
71+
This is because `B'::C'` and `B::C` are two different types,
72+
even though they have the same fully-qualified names and maybe members.
73+
This is the interoperability issue that we will face once PowerShell allow modules to be loaded into different load contexts.
74+
75+
This problem can be even worse when it comes to how `Import-Module M -Isolated` deals with M's nested modules and required modules.
76+
Unless having all nested and required modules loaded into the same custom load context,
77+
there could be interoperability issues when M calls cmdlets from its nested modules or required modules.
78+
79+
#### Mitigation by updating Type Resolution
80+
81+
We can mitigate the interoperability issue by enhance the `TypeResolver` to make it aware of the current `EngineSessionState` for a type resolution operation.
82+
If it happens in the scope of a isolated module,
83+
then `TypeResolver` first searches assemblies in the associated custom-ALC,
84+
followed by searching in the default-ALC.
85+
86+
We also need to support a new syntax for the type reference in PowerShell:
87+
88+
```powershell
89+
[ModuleName\TypeName]
90+
```
91+
92+
When the `ModuleName` part is specified, PowerShell can check if such a module exists and whether it has a custom load context.
93+
If it's a module with a custom load context, then the type resolution will search assemblies from the custom-ALC and then the default-ALC to find the `TypeName`.
94+
Otherwise, the type resolution will search the default-ALC only.
95+
96+
So in the above imagined scenario, it's possible to make it work as expected if you construct the object using `[M\B::C]` from the default-ALC.
97+
But it's non-intuitive and I think many existing scripts may fail when modules started to be loaded in separate load contexts.
98+
99+
### Caching Problem
100+
101+
PowerShell has a lot caches related to `System.Type`.
102+
Some have `Type` as the value, such as the cache in `TypeResolver` and the type cache for `TypeNameAst`.
103+
Some have `Type` as the key, such as the member tables in `DotNetAdapter` and the binder caches in DLR.
104+
105+
For the former category, the cache data structure needs to be updated so as to make sure the cache is associated with a load context.
106+
So for example, when type resolution happens for `[C]` that is running from an isolated module M,
107+
then `TypeResolver` should use the cache associated with ALC-M.
108+
109+
Another example, if a `TypeNameAst` has resolve the type via reflection in default-ALC,
110+
and the `ScriptBlock` containing that `TypeNameAst` is being executed in the context of an isolated module M,
111+
then the cached type referred by `TypeNameAst` should be voided and another resolution should be triggered.
112+
Similarly, if the `ScriptBlock` has already been compiled into LINQ expression tree and have the delegate generated,
113+
then the cached delegate needs to be voided and another compilation needs to be enforced to make sure type resolution is correct.
114+
115+
For the latter category, theoretically they will continue to work without change,
116+
because types loaded into different load contexts are different.
117+
Initially, I think we still need to replace the cache data structure with something like `ConditionalWeakTable<AssemblyLoadContext, ...>`,
118+
so that a load context can be GC collected after the corresponding module is removed.
119+
However, it turns out it's unlikely that the module load contexts can be reclaimed at all in PowerShell.
120+
We will visit that topic in the next section.
121+
122+
As you can imagine, it will be a huge work item to just getting the current caching correct.
123+
And besides, the bookkeeping to make sure caches from the first category are properly voided and rebuilt will be expensive and hurt performance.
124+
125+
### Cannot GC Module's AssemblyLoadContext
126+
127+
.NET Core 3.0 supports unloading an `AssemblyLoadContext` and all assemblies in it.
128+
Ideally, we would like to reclaim the load context associated with an isolated module when the module is removed.
129+
However, it's very likely not possible no matter how we update our caches to avoid holding an `AssemblyLoadContext` instance indefinitely.
130+
This is because Dynamic Language Runtime (DLR) also holds types in its cache, indefinitely.
131+
132+
The DLR types live in the default-ALC, and it will generate a `DynamicMetaObject` object for a late-binding operation,
133+
which is something like a key-value pair `<restrictions, expression>` that will be held in the DLR caches.
134+
This is a perf improvement feature for dynamic languages like PowerShell because it allows you to skip an expensive late-binding operation when the `restrictions` matches.
135+
That key-value pair will be held indefinitely, so once types from a custom load context is involved in a late binding operation, such as property access, method invocation and etc,
136+
it will be rooted in the GC Heap by the DLR cache, and the custom load context cannot be unloaded.
137+
138+
## Summary
139+
140+
To summarize, supporting the module-level isolation doesn't seem to be the right way to go.
141+
The interoperability issues caused by type identity would result in confusing errors that are not intuitive to resolve.
142+
The caching issues caused by the necessary changes to `TypeResolver` would make the code hard to maintain and slower to run.
143+
144+
## Alternate Proposals and Considerations
145+
146+
### Runspace-level Isolation
147+
148+
The Runspace-level isolation seems to be more promising.
149+
150+
- The whole Runspace and all assemblies get loaded in the Runspace are isolated in a custom load context.
151+
So interoperability will not be a problem unless you receive an object by invoking something on another isolated Runspace.
152+
- The existing caching needs no change.
153+
The `TypeResolver` implementation needs to be updated to first search assemblies in the Runspace load context,
154+
then search in the default load context.
155+
- Runspace load context can be reclaimed after the Runspace is closed,
156+
because the Runspace is not held on by anything other than the user code.
157+
- It fits in the server-application model better.
158+
Imagine we want to make a server process for PowerShell,
159+
and any hosts can connect to the server and request for a new session.
160+
It would be a requirement that an isolated session is created for each host so they are not interfered with each other.
161+
The Runspace-level isolation would be perfect in this scenario,
162+
since one server process would be able to satisfy multiple hosts by creating isolated sessions for them.
163+
164+
### About the scenario called out in Motivation
165+
166+
However, the Runspace-level isolation doesn't help the motivation called out above,
167+
because it's still not possible to load same assemblies with different versions in the same Runspace load context.
168+
169+
There is probably an easier way to address that scenario -- register a handler to `AssemblyLoadContext.AssemblyResolve` event.
170+
The resolving process is as follows:
171+
172+
- When `Assembly.Load` fails, the `AssemblyResolve` event will be triggered.
173+
- The event arg passed in contains the requesting assembly,
174+
so we can get the directory where the requesting assembly locates.
175+
- Search in that particular directory to find the exact match of the requested assembly.
176+
- if found, load it into the load context of the requesting assembly.
177+
178+
Yes, this can also cause interoperability issues,
179+
but the chance that a user is affected by it should be low compared to supporting `Import-Module -Isolated`,
180+
because only the conflicting assembly is loaded into a separate load context.
181+
182+
By far, only the assembly `Newtonsoft.Json` is reported as the conflicting assembly among different modules,
183+
so chances are only this assembly will be loaded into a separate load context for most cases.

0 commit comments

Comments
 (0)