Dispose per-instance services created by factories#47951
Dispose per-instance services created by factories#47951sharwell merged 9 commits intodotnet:mainfrom
Conversation
22db5a3 to
dab1986
Compare
genlu
left a comment
There was a problem hiding this comment.
The change makes sense to me. I'm curious what problem not disposing those Worksapce/Language services causes today.
It leads to direct coupling of roslyn/src/Workspaces/Core/Portable/Workspace/Workspace.cs Lines 376 to 377 in f67cb8a There are also two new services that need this support coming in separate pull requests. |
|
On this PR i'm going to defer to @jasonmalinowski . Primarily because i'm not a fan of IDisposable and/or making more parts of our system have that pattern spread through it. I find it a fairly high mental and design tax to take on that now makes it more difficult for all consumptive uses of these APIs. My preference would strongly be that we only do this as a last resort, and only if there aren't other suitable approaches that can avoid this. HOwever, i'll defer to @jasonmalinowski here if he feels this is in line with standard patterns and practices for Roslyn, Mef and the greater .net ecosystem. If so, in balance, this is perhaps acceptable given that we would not be forging our own path here and we be consistent with a larger whole. |
|
@jasonmalinowski @CyrusNajmabadi This is updated and ready to merge. |
|
I can look on Monday thanks. |
|
@CyrusNajmabadi note that there are no significant functional changes since the last review from you. Just FYI. |
jasonmalinowski
left a comment
There was a problem hiding this comment.
This is a nice cleanup for a long-neglected area, so thanks for taking a look at it.
If I have one "concern", I hope we don't have cases where people "helpfully" see the services they might get back from GetService are disposable, and stick it in a using statement and now they're disposing objects underneath other people. Consider any of the disposable lifetime tracking analyzers that might be out there. It could be mitigated by instead introducing something like IDisposableWorkspaceServiceFactory that has a second method that takes the service object (and it's up to the implementer to under the covers call some explicit cleanup method), so that way the service objects themselves aren't disposable. But that's kinda icky though.
Maybe add a remark on GetService that if the object returned is disposable, don't dispose it?
| { | ||
| internal static class MefUtilities | ||
| { | ||
| public static void DisposeWithExceptionTracking<T>(T instance, [NotNullIfNotNull("exceptions")] ref List<Exception>? exceptions) |
There was a problem hiding this comment.
The nullable annotation there on exceptions is intentional and not a tautology?
There was a problem hiding this comment.
➡️ Without this attribute, the method would be allowed to reassign null to a non-null storage location.
| /// <strong><see cref="IWorkspaceServiceFactory"/> instance constructed externally (e.g. MEF):</strong> Owned by | ||
| /// the external source, and will not be automatically disposed when <see cref="Workspace"/> is disposed. |
There was a problem hiding this comment.
This line confused me a bit, since the IWorkspaceServiceFactory is "definitely" not owned by the workspace, i.e. this bullet might cause more confusion than good.
There was a problem hiding this comment.
⏱️ I'll see if I can clarify that the only option for IWorkspaceServiceFactory is that it's owned outside the workspace.
| /// <para>Workspace services which implement <see cref="IDisposable"/> are considered ownable, in which case the | ||
| /// owner is responsible for disposing of owned instances when they are no longer in use. When | ||
| /// <see cref="IWorkspaceService"/> or <see cref="IWorkspaceServiceFactory"/> instances are provided directly to the | ||
| /// <see cref="HostWorkspaceServices"/>, the owner of the instances is the type or container (e.g. a MEF export | ||
| /// provider) which created the instances. For the specific case of ownable workspace services created by a factory | ||
| /// (i.e. instances returned by <see cref="IWorkspaceServiceFactory.CreateService"/>), the <see cref="Workspace"/> | ||
| /// is considered the owner of the resulting instance and is expected to be disposed during the call to | ||
| /// <see cref="Dispose"/>.</para> | ||
| /// | ||
| /// <para><strong>Summary of lifetime rules</strong></para> | ||
| /// | ||
| /// <list type="bullet"> | ||
| /// <item><description> | ||
| /// <strong><see cref="IWorkspaceService"/> instance constructed externally (e.g. MEF):</strong> Owned by the | ||
| /// external source, and will not be automatically disposed when <see cref="Workspace"/> is disposed. | ||
| /// </description></item> | ||
| /// <item><description> | ||
| /// <strong><see cref="IWorkspaceServiceFactory"/> instance constructed externally (e.g. MEF):</strong> Owned by | ||
| /// the external source, and will not be automatically disposed when <see cref="Workspace"/> is disposed. | ||
| /// </description></item> | ||
| /// <item><description> | ||
| /// <strong><see cref="IWorkspaceService"/> instance constructed by <see cref="IWorkspaceServiceFactory"/> within | ||
| /// the context of <see cref="HostWorkspaceServices"/>:</strong> Owned by <see cref="Workspace"/>, and | ||
| /// <strong>will</strong> be automatically disposed when <see cref="Workspace"/> is disposed. | ||
| /// </description></item> | ||
| /// </list> | ||
| /// </remarks> |
There was a problem hiding this comment.
It feels like this can really be summarized as:
- Since this object created them, this object owns disposable IWorkspaceServices that are created from IWorkspaceFactoryServices, and will dispose them.
- Workspace services not created from factories and the factories themselves are not disposed.
| // Directly dispose IRemoteHostClientProvider if necessary. This is a test hook to ensure RemoteWorkspace | ||
| // gets disposed in unit tests as soon as TestWorkspace gets disposed. This would be superseded by direct | ||
| // support for IDisposable in https://github.com/dotnet/roslyn/pull/47951. | ||
| if (Services.GetService<IRemoteHostClientProvider>() is IDisposable disposableService) |
There was a problem hiding this comment.
I love the implication here that we'll potentially create the service only to immediately dispose it if it hadn't already been created.
There was a problem hiding this comment.
➡️ Yes, and I tried to call out the benefits of tracking object creation in the top-level pull request comment as well.
| List<Exception> exceptions = null; | ||
| foreach (var service in disposableServices) | ||
| { | ||
| MefUtilities.DisposeWithExceptionTracking(service, ref exceptions); | ||
| } | ||
|
|
||
| if (exceptions is not null) | ||
| { | ||
| throw new AggregateException(CompilerExtensionsResources.Instantiated_parts_threw_exceptions_from_IDisposable_Dispose, exceptions); | ||
| } |
There was a problem hiding this comment.
Consider refactoring MefUtilities.DisposeWithExceptionTracking to just do this entire pattern of looping and throwing, since we'll need the same thing in the other place too.
| // MEF workspace service instances created by a factory are not owned by the MEF catalog or disposed | ||
| // when the MEF catalog is disposed. Whenever we are potentially going to create an instance of a | ||
| // service provided by a factory, we need to check if the resulting service implements IDisposable. The | ||
| // specific conditions here are: | ||
| // | ||
| // * usesFactory: This is true when the workspace service is provided by a factory. Services provided | ||
| // directly are owned by the MEF catalog so they do not need to be tracked by the workspace. | ||
| // * IsValueCreated: This will be false at least once prior to accessing the lazy value. Once the value | ||
| // is known to be created, we no longer need to try adding it to _ownedDisposableServices, so we use a | ||
| // lock-free fast path. | ||
| var checkAddDisposable = service.usesFactory && !service.lazyService.IsValueCreated; | ||
|
|
||
| var serviceInstance = (TWorkspaceService)service.lazyService.Value; | ||
| if (checkAddDisposable && serviceInstance is IDisposable disposable) | ||
| { | ||
| lock (_gate) | ||
| { | ||
| _ownedDisposableServices.Add(disposable); | ||
| } | ||
| } |
There was a problem hiding this comment.
Consider extracting and putting this into MefUtilities since it's the same as the other use.
There was a problem hiding this comment.
(although I guess it's a strange to pass the lock out...)
| @@ -523,13 +522,9 @@ protected virtual void Dispose(bool finalize) | |||
|
|
|||
There was a problem hiding this comment.
GitHub won't let me leave a comment the line above this, but an easy follow up to this PR is the this.Services.GetService()?.Stop(); line and making that also be disposable based.
There was a problem hiding this comment.
➡️ This is already part of follow-up #48068.
Typically workspace services are defined with an interface, and exported with an implementation. For disposable service instances, the interface would not inherit |
This change makes
MefWorkspaceServicesandMefLanguageServicesbehave like MEF itself (specifically, itsExportProvider).IDisposableare disposed when theExportProvideris disposed. This includes workspace services and language services which are exported directly (as opposed be being exported through a factory)IDisposableare disposed automatically when the corresponding workspace is disposed.The automation of this functionality is primarily valuable because we no longer need to keep track of whether or not a part was created in order to dispose it. Only parts that have already been created are disposed by the workspace.